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 @file:Suppress("DEPRECATION")
18 
19 package androidx.compose.foundation.layout
20 
21 import androidx.annotation.FloatRange
22 import androidx.collection.IntIntPair
23 import androidx.collection.mutableIntListOf
24 import androidx.collection.mutableIntObjectMapOf
25 import androidx.compose.foundation.layout.internal.requirePrecondition
26 import androidx.compose.runtime.Composable
27 import androidx.compose.runtime.Stable
28 import androidx.compose.runtime.collection.MutableVector
29 import androidx.compose.runtime.collection.mutableVectorOf
30 import androidx.compose.runtime.remember
31 import androidx.compose.ui.Alignment
32 import androidx.compose.ui.Modifier
33 import androidx.compose.ui.layout.IntrinsicMeasurable
34 import androidx.compose.ui.layout.IntrinsicMeasureScope
35 import androidx.compose.ui.layout.Layout
36 import androidx.compose.ui.layout.Measurable
37 import androidx.compose.ui.layout.MeasurePolicy
38 import androidx.compose.ui.layout.MeasureResult
39 import androidx.compose.ui.layout.MeasureScope
40 import androidx.compose.ui.layout.MultiContentMeasurePolicy
41 import androidx.compose.ui.layout.Placeable
42 import androidx.compose.ui.node.ModifierNodeElement
43 import androidx.compose.ui.node.ParentDataModifierNode
44 import androidx.compose.ui.platform.InspectorInfo
45 import androidx.compose.ui.unit.Constraints
46 import androidx.compose.ui.unit.Density
47 import androidx.compose.ui.unit.Dp
48 import androidx.compose.ui.unit.LayoutDirection
49 import androidx.compose.ui.util.fastCoerceAtLeast
50 import androidx.compose.ui.util.fastCoerceIn
51 import androidx.compose.ui.util.fastForEachIndexed
52 import kotlin.math.ceil
53 import kotlin.math.max
54 import kotlin.math.min
55 
56 /**
57  * [FlowRow] is a layout that fills items from left to right (ltr) in LTR layouts or right to left
58  * (rtl) in RTL layouts and when it runs out of space, moves to the next "row" or "line" positioned
59  * on the bottom, and then continues filling items until the items run out.
60  *
61  * Example:
62  *
63  * @sample androidx.compose.foundation.layout.samples.SimpleFlowRow
64  *
65  * When a Modifier [RowScope.weight] is provided, it scales the item based on the number items that
66  * fall on the row it was placed in.
67  *
68  * Note that if two or more Text components are placed in a [Row], normally they should be aligned
69  * by their first baselines. [FlowRow] as a general purpose container does not do it automatically
70  * so developers need to handle this manually. This is achieved by adding a
71  * [RowScope.alignByBaseline] modifier to every such Text component. By default this modifier aligns
72  * by [androidx.compose.ui.layout.FirstBaseline]. If, however, you need to align Texts by
73  * [androidx.compose.ui.layout.LastBaseline] for example, use a more general [RowScope.alignBy]
74  * modifier.
75  *
76  * @param modifier The modifier to be applied to the Row.
77  * @param horizontalArrangement The horizontal arrangement of the layout's children.
78  * @param verticalArrangement The vertical arrangement of the layout's virtual rows.
79  * @param itemVerticalAlignment The cross axis/vertical alignment of an item in the column.
80  * @param maxItemsInEachRow The maximum number of items per row
81  * @param maxLines The max number of rows
82  * @param overflow The strategy to handle overflowing items
83  * @param content The content as a [RowScope]
84  * @see FlowColumn
85  * @see [androidx.compose.foundation.layout.Row]
86  */
87 @Deprecated("The overflow parameter has been deprecated")
88 @Composable
89 @ExperimentalLayoutApi
90 fun FlowRow(
91     modifier: Modifier = Modifier,
92     horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
93     verticalArrangement: Arrangement.Vertical = Arrangement.Top,
94     itemVerticalAlignment: Alignment.Vertical = Alignment.Top,
95     maxItemsInEachRow: Int = Int.MAX_VALUE,
96     maxLines: Int = Int.MAX_VALUE,
97     overflow: FlowRowOverflow = FlowRowOverflow.Clip,
98     content: @Composable FlowRowScope.() -> Unit
99 ) {
100     val overflowState = remember(overflow) { overflow.createOverflowState() }
101     val measurePolicy =
102         rowMeasurementMultiContentHelper(
103             horizontalArrangement,
104             verticalArrangement,
105             itemVerticalAlignment,
106             maxItemsInEachRow,
107             maxLines,
108             overflowState
109         )
110     val list: List<@Composable () -> Unit> =
111         remember(overflow, content, maxLines) {
112             val mutableList: MutableList<@Composable () -> Unit> = mutableListOf()
113             mutableList.add { FlowRowScopeInstance.content() }
114             overflow.addOverflowComposables(overflowState, mutableList)
115             mutableList
116         }
117 
118     Layout(contents = list, measurePolicy = measurePolicy, modifier = modifier)
119 }
120 
121 /**
122  * [FlowRow] is a layout that fills items from left to right (ltr) in LTR layouts or right to left
123  * (rtl) in RTL layouts and when it runs out of space, moves to the next "row" or "line" positioned
124  * on the bottom, and then continues filling items until the items run out.
125  *
126  * Example:
127  *
128  * @sample androidx.compose.foundation.layout.samples.SimpleFlowRow
129  *
130  * When a Modifier [RowScope.weight] is provided, it scales the item based on the number items that
131  * fall on the row it was placed in.
132  *
133  * Note that if two or more Text components are placed in a [Row], normally they should be aligned
134  * by their first baselines. [FlowRow] as a general purpose container does not do it automatically
135  * so developers need to handle this manually. This is achieved by adding a
136  * [RowScope.alignByBaseline] modifier to every such Text component. By default this modifier aligns
137  * by [androidx.compose.ui.layout.FirstBaseline]. If, however, you need to align Texts by
138  * [androidx.compose.ui.layout.LastBaseline] for example, use a more general [RowScope.alignBy]
139  * modifier.
140  *
141  * @param modifier The modifier to be applied to the Row.
142  * @param horizontalArrangement The horizontal arrangement of the layout's children.
143  * @param verticalArrangement The vertical arrangement of the layout's virtual rows.
144  * @param itemVerticalAlignment The cross axis/vertical alignment of an item in the column.
145  * @param maxItemsInEachRow The maximum number of items per row
146  * @param maxLines The max number of rows
147  * @param content The content as a [RowScope]
148  * @see FlowColumn
149  * @see [androidx.compose.foundation.layout.Row]
150  */
151 @OptIn(ExperimentalLayoutApi::class)
152 @Composable
FlowRownull153 fun FlowRow(
154     modifier: Modifier = Modifier,
155     horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
156     verticalArrangement: Arrangement.Vertical = Arrangement.Top,
157     itemVerticalAlignment: Alignment.Vertical = Alignment.Top,
158     maxItemsInEachRow: Int = Int.MAX_VALUE,
159     maxLines: Int = Int.MAX_VALUE,
160     content: @Composable FlowRowScope.() -> Unit
161 ) =
162     FlowRow(
163         modifier,
164         horizontalArrangement,
165         verticalArrangement,
166         itemVerticalAlignment,
167         maxItemsInEachRow,
168         maxLines,
169         FlowRowOverflow.Clip,
170         content,
171     )
172 
173 /**
174  * [FlowColumn] is a layout that fills items from top to bottom, and when it runs out of space on
175  * the bottom, moves to the next "column" or "line" on the right or left based on ltr or rtl
176  * layouts, and then continues filling items from top to bottom.
177  *
178  * It supports ltr in LTR layouts, by placing the first column to the left, and then moving to the
179  * right It supports rtl in RTL layouts, by placing the first column to the right, and then moving
180  * to the left
181  *
182  * Example:
183  *
184  * @sample androidx.compose.foundation.layout.samples.SimpleFlowColumn
185  *
186  * When a Modifier [ColumnScope.weight] is provided, it scales the item based on the number items
187  * that fall on the column it was placed in.
188  *
189  * @param modifier The modifier to be applied to the Row.
190  * @param verticalArrangement The vertical arrangement of the layout's children.
191  * @param horizontalArrangement The horizontal arrangement of the layout's virtual columns
192  * @param itemHorizontalAlignment The cross axis/horizontal alignment of an item in the column.
193  * @param maxItemsInEachColumn The maximum number of items per column
194  * @param maxLines The max number of rows
195  * @param overflow The strategy to handle overflowing items
196  * @param content The content as a [ColumnScope]
197  * @see FlowRow
198  * @see ContextualFlowColumn
199  * @see [androidx.compose.foundation.layout.Column]
200  */
201 @Deprecated("The overflow parameter has been deprecated")
202 @Composable
203 @ExperimentalLayoutApi
204 fun FlowColumn(
205     modifier: Modifier = Modifier,
206     verticalArrangement: Arrangement.Vertical = Arrangement.Top,
207     horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
208     itemHorizontalAlignment: Alignment.Horizontal = Alignment.Start,
209     maxItemsInEachColumn: Int = Int.MAX_VALUE,
210     maxLines: Int = Int.MAX_VALUE,
211     overflow: FlowColumnOverflow = FlowColumnOverflow.Clip,
212     content: @Composable FlowColumnScope.() -> Unit
213 ) {
214     val overflowState = remember(overflow) { overflow.createOverflowState() }
215     val measurePolicy =
216         columnMeasurementMultiContentHelper(
217             verticalArrangement,
218             horizontalArrangement,
219             itemHorizontalAlignment,
220             maxItemsInEachColumn,
221             maxLines,
222             overflowState
223         )
224     val list: List<@Composable () -> Unit> =
225         remember(overflow, content, maxLines) {
226             val mutableList: MutableList<@Composable () -> Unit> = mutableListOf()
227             mutableList.add { FlowColumnScopeInstance.content() }
228             overflow.addOverflowComposables(overflowState, mutableList)
229             mutableList
230         }
231     Layout(contents = list, measurePolicy = measurePolicy, modifier = modifier)
232 }
233 
234 /**
235  * [FlowColumn] is a layout that fills items from top to bottom, and when it runs out of space on
236  * the bottom, moves to the next "column" or "line" on the right or left based on ltr or rtl
237  * layouts, and then continues filling items from top to bottom.
238  *
239  * It supports ltr in LTR layouts, by placing the first column to the left, and then moving to the
240  * right It supports rtl in RTL layouts, by placing the first column to the right, and then moving
241  * to the left
242  *
243  * Example:
244  *
245  * @sample androidx.compose.foundation.layout.samples.SimpleFlowColumn
246  *
247  * When a Modifier [ColumnScope.weight] is provided, it scales the item based on the number items
248  * that fall on the column it was placed in.
249  *
250  * @param modifier The modifier to be applied to the Row.
251  * @param verticalArrangement The vertical arrangement of the layout's children.
252  * @param horizontalArrangement The horizontal arrangement of the layout's virtual columns
253  * @param itemHorizontalAlignment The cross axis/horizontal alignment of an item in the column.
254  * @param maxItemsInEachColumn The maximum number of items per column
255  * @param maxLines The max number of rows
256  * @param content The content as a [ColumnScope]
257  * @see FlowRow
258  * @see [androidx.compose.foundation.layout.Column]
259  */
260 @OptIn(ExperimentalLayoutApi::class)
261 @Composable
FlowColumnnull262 fun FlowColumn(
263     modifier: Modifier = Modifier,
264     verticalArrangement: Arrangement.Vertical = Arrangement.Top,
265     horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
266     itemHorizontalAlignment: Alignment.Horizontal = Alignment.Start,
267     maxItemsInEachColumn: Int = Int.MAX_VALUE,
268     maxLines: Int = Int.MAX_VALUE,
269     content: @Composable FlowColumnScope.() -> Unit
270 ) =
271     FlowColumn(
272         modifier,
273         verticalArrangement,
274         horizontalArrangement,
275         itemHorizontalAlignment,
276         maxItemsInEachColumn,
277         maxLines,
278         FlowColumnOverflow.Clip,
279         content,
280     )
281 
282 /** Scope for the children of [FlowRow]. */
283 @LayoutScopeMarker
284 @Stable
285 interface FlowRowScope : RowScope {
286     /**
287      * Have the item fill (possibly only partially) the max height of the tallest item in the row it
288      * was placed in, within the [FlowRow].
289      *
290      * @param fraction The fraction of the max height of the tallest item between `0` and `1`,
291      *   inclusive.
292      *
293      * Example usage:
294      *
295      * @sample androidx.compose.foundation.layout.samples.SimpleFlowRow_EqualHeight
296      */
297     @ExperimentalLayoutApi
298     fun Modifier.fillMaxRowHeight(
299         @FloatRange(from = 0.0, to = 1.0) fraction: Float = 1f,
300     ): Modifier
301 }
302 
303 /** Scope for the overflow [FlowRow]. */
304 @LayoutScopeMarker
305 @Stable
306 @ExperimentalLayoutApi
307 interface FlowRowOverflowScope : FlowRowScope {
308     /**
309      * Total Number of Items available to show in [FlowRow] This includes items that may not be
310      * displayed.
311      *
312      * In [ContextualFlowRow], this matches the [ContextualFlowRow]'s `itemCount` parameter
313      */
314     @ExperimentalLayoutApi val totalItemCount: Int
315 
316     /** Total Number of Items displayed in the [FlowRow] */
317     @ExperimentalLayoutApi val shownItemCount: Int
318 }
319 
320 /** Scope for the children of [FlowColumn]. */
321 @LayoutScopeMarker
322 @Stable
323 interface FlowColumnScope : ColumnScope {
324     /**
325      * Have the item fill (possibly only partially) the max width of the widest item in the column
326      * it was placed in, within the [FlowColumn].
327      *
328      * @param fraction The fraction of the max width of the widest item between `0` and `1`,
329      *   inclusive.
330      *
331      * Example usage:
332      *
333      * @sample androidx.compose.foundation.layout.samples.SimpleFlowColumn_EqualWidth
334      */
335     @ExperimentalLayoutApi
fillMaxColumnWidthnull336     fun Modifier.fillMaxColumnWidth(
337         @FloatRange(from = 0.0, to = 1.0) fraction: Float = 1f,
338     ): Modifier
339 }
340 
341 /** Scope for the overflow [FlowColumn]. */
342 @LayoutScopeMarker
343 @Stable
344 @ExperimentalLayoutApi
345 interface FlowColumnOverflowScope : FlowColumnScope {
346     /**
347      * Total Number of Items available to show in [FlowColumn] This includes items that may not be
348      * displayed.
349      *
350      * In [ContextualFlowColumn], this matches the [ContextualFlowColumn]'s `itemCount` parameter
351      */
352     @ExperimentalLayoutApi val totalItemCount: Int
353 
354     /** Total Number of Items displayed in the [FlowColumn] */
355     @ExperimentalLayoutApi val shownItemCount: Int
356 }
357 
358 @OptIn(ExperimentalLayoutApi::class)
359 internal object FlowRowScopeInstance : RowScope by RowScopeInstance, FlowRowScope {
fillMaxRowHeightnull360     override fun Modifier.fillMaxRowHeight(fraction: Float): Modifier {
361         requirePrecondition(fraction >= 0.0f && fraction <= 1.0f) {
362             "invalid fraction $fraction; must be >= 0 and <= 1.0"
363         }
364         return this.then(
365             FillCrossAxisSizeElement(
366                 fraction = fraction,
367             )
368         )
369     }
370 }
371 
372 @OptIn(ExperimentalLayoutApi::class)
373 internal class FlowRowOverflowScopeImpl(private val state: FlowLayoutOverflowState) :
374     FlowRowScope by FlowRowScopeInstance, FlowRowOverflowScope {
<lambda>null375     override val totalItemCount: Int by lazyInt { state.itemCount }
376 
<lambda>null377     override val shownItemCount: Int by lazyInt(state.shownItemLazyErrorMessage) { state.itemShown }
378 }
379 
380 @OptIn(ExperimentalLayoutApi::class)
381 internal class FlowColumnOverflowScopeImpl(private val state: FlowLayoutOverflowState) :
382     FlowColumnScope by FlowColumnScopeInstance, FlowColumnOverflowScope {
<lambda>null383     override val totalItemCount: Int by lazyInt { state.itemCount }
384 
<lambda>null385     override val shownItemCount: Int by lazyInt(state.shownItemLazyErrorMessage) { state.itemShown }
386 }
387 
388 @OptIn(ExperimentalLayoutApi::class)
389 internal object FlowColumnScopeInstance : ColumnScope by ColumnScopeInstance, FlowColumnScope {
fillMaxColumnWidthnull390     override fun Modifier.fillMaxColumnWidth(fraction: Float): Modifier {
391         requirePrecondition(fraction >= 0.0f && fraction <= 1.0f) {
392             "invalid fraction $fraction; must be >= 0 and <= 1.0"
393         }
394         return this.then(
395             FillCrossAxisSizeElement(
396                 fraction = fraction,
397             )
398         )
399     }
400 }
401 
402 internal data class FlowLayoutData(var fillCrossAxisFraction: Float)
403 
404 internal class FillCrossAxisSizeNode(
405     var fraction: Float,
406 ) : ParentDataModifierNode, Modifier.Node() {
modifyParentDatanull407     override fun Density.modifyParentData(parentData: Any?) =
408         ((parentData as? RowColumnParentData) ?: RowColumnParentData()).also {
409             it.flowLayoutData = it.flowLayoutData ?: FlowLayoutData(fraction)
410             it.flowLayoutData!!.fillCrossAxisFraction = fraction
411         }
412 }
413 
414 internal class FillCrossAxisSizeElement(val fraction: Float) :
415     ModifierNodeElement<FillCrossAxisSizeNode>() {
createnull416     override fun create(): FillCrossAxisSizeNode {
417         return FillCrossAxisSizeNode(fraction)
418     }
419 
updatenull420     override fun update(node: FillCrossAxisSizeNode) {
421         node.fraction = fraction
422     }
423 
inspectablePropertiesnull424     override fun InspectorInfo.inspectableProperties() {
425         name = "fraction"
426         value = fraction
427         properties["fraction"] = fraction
428     }
429 
hashCodenull430     override fun hashCode(): Int {
431         var result = fraction.hashCode()
432         result *= 31
433         return result
434     }
435 
equalsnull436     override fun equals(other: Any?): Boolean {
437         if (this === other) return true
438         val otherModifier = other as? FillCrossAxisSizeNode ?: return false
439         return fraction == otherModifier.fraction
440     }
441 }
442 
443 @OptIn(ExperimentalLayoutApi::class)
444 @PublishedApi
445 @Composable
rowMeasurementHelpernull446 internal fun rowMeasurementHelper(
447     horizontalArrangement: Arrangement.Horizontal,
448     verticalArrangement: Arrangement.Vertical,
449     maxItemsInMainAxis: Int,
450 ): MeasurePolicy {
451     return remember(
452         horizontalArrangement,
453         verticalArrangement,
454         maxItemsInMainAxis,
455     ) {
456         val measurePolicy =
457             FlowMeasurePolicy(
458                 isHorizontal = true,
459                 horizontalArrangement = horizontalArrangement,
460                 mainAxisSpacing = horizontalArrangement.spacing,
461                 crossAxisAlignment = CROSS_AXIS_ALIGNMENT_TOP,
462                 verticalArrangement = verticalArrangement,
463                 crossAxisArrangementSpacing = verticalArrangement.spacing,
464                 maxItemsInMainAxis = maxItemsInMainAxis,
465                 maxLines = Int.MAX_VALUE,
466                 overflow = FlowRowOverflow.Visible.createOverflowState()
467             )
468                 as MultiContentMeasurePolicy
469 
470         MeasurePolicy { measurables, constraints ->
471             with(measurePolicy) { this@MeasurePolicy.measure(listOf(measurables), constraints) }
472         }
473     }
474 }
475 
476 @OptIn(ExperimentalLayoutApi::class)
477 @Composable
rowMeasurementMultiContentHelpernull478 internal fun rowMeasurementMultiContentHelper(
479     horizontalArrangement: Arrangement.Horizontal,
480     verticalArrangement: Arrangement.Vertical,
481     itemVerticalAlignment: Alignment.Vertical,
482     maxItemsInMainAxis: Int,
483     maxLines: Int,
484     overflowState: FlowLayoutOverflowState,
485 ): MultiContentMeasurePolicy {
486     return remember(
487         horizontalArrangement,
488         verticalArrangement,
489         itemVerticalAlignment,
490         maxItemsInMainAxis,
491         maxLines,
492         overflowState
493     ) {
494         FlowMeasurePolicy(
495             isHorizontal = true,
496             horizontalArrangement = horizontalArrangement,
497             mainAxisSpacing = horizontalArrangement.spacing,
498             crossAxisAlignment = CrossAxisAlignment.vertical(itemVerticalAlignment),
499             verticalArrangement = verticalArrangement,
500             crossAxisArrangementSpacing = verticalArrangement.spacing,
501             maxItemsInMainAxis = maxItemsInMainAxis,
502             maxLines = maxLines,
503             overflow = overflowState
504         )
505     }
506 }
507 
508 @OptIn(ExperimentalLayoutApi::class)
509 @PublishedApi
510 @Composable
columnMeasurementHelpernull511 internal fun columnMeasurementHelper(
512     verticalArrangement: Arrangement.Vertical,
513     horizontalArrangement: Arrangement.Horizontal,
514     maxItemsInMainAxis: Int,
515 ): MeasurePolicy {
516     return remember(
517         verticalArrangement,
518         horizontalArrangement,
519         maxItemsInMainAxis,
520     ) {
521         val measurePolicy =
522             FlowMeasurePolicy(
523                 isHorizontal = false,
524                 verticalArrangement = verticalArrangement,
525                 mainAxisSpacing = verticalArrangement.spacing,
526                 crossAxisAlignment = CROSS_AXIS_ALIGNMENT_START,
527                 horizontalArrangement = horizontalArrangement,
528                 crossAxisArrangementSpacing = horizontalArrangement.spacing,
529                 maxItemsInMainAxis = maxItemsInMainAxis,
530                 maxLines = Int.MAX_VALUE,
531                 overflow = FlowRowOverflow.Visible.createOverflowState()
532             )
533         MeasurePolicy { measurables, constraints ->
534             with(measurePolicy) { this@MeasurePolicy.measure(listOf(measurables), constraints) }
535         }
536     }
537 }
538 
539 @Composable
columnMeasurementMultiContentHelpernull540 internal fun columnMeasurementMultiContentHelper(
541     verticalArrangement: Arrangement.Vertical,
542     horizontalArrangement: Arrangement.Horizontal,
543     itemHorizontalAlignment: Alignment.Horizontal,
544     maxItemsInMainAxis: Int,
545     maxLines: Int,
546     overflowState: FlowLayoutOverflowState
547 ): MultiContentMeasurePolicy {
548     return remember(
549         verticalArrangement,
550         horizontalArrangement,
551         itemHorizontalAlignment,
552         maxItemsInMainAxis,
553         maxLines,
554         overflowState
555     ) {
556         FlowMeasurePolicy(
557             isHorizontal = false,
558             verticalArrangement = verticalArrangement,
559             mainAxisSpacing = verticalArrangement.spacing,
560             crossAxisAlignment = CrossAxisAlignment.horizontal(itemHorizontalAlignment),
561             horizontalArrangement = horizontalArrangement,
562             crossAxisArrangementSpacing = horizontalArrangement.spacing,
563             maxItemsInMainAxis = maxItemsInMainAxis,
564             maxLines = maxLines,
565             overflow = overflowState
566         )
567     }
568 }
569 
570 internal interface FlowLineMeasurePolicy : RowColumnMeasurePolicy {
571     val isHorizontal: Boolean
572     val horizontalArrangement: Arrangement.Horizontal
573     val verticalArrangement: Arrangement.Vertical
574     val crossAxisAlignment: CrossAxisAlignment
575 
mainAxisSizenull576     override fun Placeable.mainAxisSize() = if (isHorizontal) measuredWidth else measuredHeight
577 
578     override fun Placeable.crossAxisSize() = if (isHorizontal) measuredHeight else measuredWidth
579 
580     override fun createConstraints(
581         mainAxisMin: Int,
582         crossAxisMin: Int,
583         mainAxisMax: Int,
584         crossAxisMax: Int,
585         isPrioritizing: Boolean
586     ): Constraints {
587         return if (isHorizontal) {
588             createRowConstraints(
589                 isPrioritizing,
590                 mainAxisMin,
591                 crossAxisMin,
592                 mainAxisMax,
593                 crossAxisMax,
594             )
595         } else {
596             createColumnConstraints(
597                 isPrioritizing,
598                 mainAxisMin,
599                 crossAxisMin,
600                 mainAxisMax,
601                 crossAxisMax,
602             )
603         }
604     }
605 
placeHelpernull606     override fun placeHelper(
607         placeables: Array<Placeable?>,
608         measureScope: MeasureScope,
609         beforeCrossAxisAlignmentLine: Int,
610         mainAxisPositions: IntArray,
611         mainAxisLayoutSize: Int,
612         crossAxisLayoutSize: Int,
613         crossAxisOffset: IntArray?,
614         currentLineIndex: Int,
615         startIndex: Int,
616         endIndex: Int
617     ): MeasureResult {
618         with(measureScope) {
619             val width: Int
620             val height: Int
621             if (isHorizontal) {
622                 width = mainAxisLayoutSize
623                 height = crossAxisLayoutSize
624             } else {
625                 width = crossAxisLayoutSize
626                 height = mainAxisLayoutSize
627             }
628             val layoutDirection =
629                 if (isHorizontal) {
630                     LayoutDirection.Ltr
631                 } else {
632                     layoutDirection
633                 }
634             return layout(width, height) {
635                 val crossAxisLineOffset = crossAxisOffset?.get(currentLineIndex) ?: 0
636                 for (i in startIndex until endIndex) {
637                     val placeable = placeables[i]!!
638                     val crossAxisPosition =
639                         getCrossAxisPosition(
640                             placeable,
641                             crossAxisLayoutSize,
642                             layoutDirection,
643                             beforeCrossAxisAlignmentLine
644                         ) + crossAxisLineOffset
645                     if (isHorizontal) {
646                         placeable.place(mainAxisPositions[i - startIndex], crossAxisPosition)
647                     } else {
648                         placeable.place(crossAxisPosition, mainAxisPositions[i - startIndex])
649                     }
650                 }
651             }
652         }
653     }
654 
getCrossAxisPositionnull655     fun getCrossAxisPosition(
656         placeable: Placeable,
657         crossAxisLayoutSize: Int,
658         layoutDirection: LayoutDirection,
659         beforeCrossAxisAlignmentLine: Int
660     ): Int {
661         val childCrossAlignment =
662             placeable.rowColumnParentData?.crossAxisAlignment ?: crossAxisAlignment
663         return childCrossAlignment.align(
664             size = crossAxisLayoutSize - placeable.crossAxisSize(),
665             layoutDirection = layoutDirection,
666             placeable = placeable,
667             beforeCrossAxisAlignmentLine = beforeCrossAxisAlignmentLine
668         )
669     }
670 
populateMainAxisPositionsnull671     override fun populateMainAxisPositions(
672         mainAxisLayoutSize: Int,
673         childrenMainAxisSize: IntArray,
674         mainAxisPositions: IntArray,
675         measureScope: MeasureScope
676     ) {
677         if (isHorizontal) {
678             with(horizontalArrangement) {
679                 measureScope.arrange(
680                     mainAxisLayoutSize,
681                     childrenMainAxisSize,
682                     measureScope.layoutDirection,
683                     mainAxisPositions
684                 )
685             }
686         } else {
687             with(verticalArrangement) {
688                 measureScope.arrange(
689                     mainAxisLayoutSize,
690                     childrenMainAxisSize,
691                     mainAxisPositions,
692                 )
693             }
694         }
695     }
696 }
697 
698 /** Returns a Flow Measure Policy */
699 @OptIn(ExperimentalLayoutApi::class)
700 private data class FlowMeasurePolicy(
701     override val isHorizontal: Boolean,
702     override val horizontalArrangement: Arrangement.Horizontal,
703     override val verticalArrangement: Arrangement.Vertical,
704     private val mainAxisSpacing: Dp,
705     override val crossAxisAlignment: CrossAxisAlignment,
706     private val crossAxisArrangementSpacing: Dp,
707     private val maxItemsInMainAxis: Int,
708     private val maxLines: Int,
709     private val overflow: FlowLayoutOverflowState,
710 ) : MultiContentMeasurePolicy, FlowLineMeasurePolicy {
711 
measurenull712     override fun MeasureScope.measure(
713         measurables: List<List<Measurable>>,
714         constraints: Constraints
715     ): MeasureResult {
716         if (
717             maxLines == 0 ||
718                 maxItemsInMainAxis == 0 ||
719                 measurables.isEmpty() ||
720                 constraints.maxHeight == 0 &&
721                     overflow.type != FlowLayoutOverflow.OverflowType.Visible
722         ) {
723             return layout(0, 0) {}
724         }
725         val list = measurables.first()
726         if (list.isEmpty()) {
727             return layout(0, 0) {}
728         }
729         val seeMoreMeasurable = measurables.getOrNull(1)?.firstOrNull()
730         val collapseMeasurable = measurables.getOrNull(2)?.firstOrNull()
731         overflow.itemCount = list.size
732         overflow.setOverflowMeasurables(
733             this@FlowMeasurePolicy,
734             seeMoreMeasurable,
735             collapseMeasurable,
736             constraints,
737         )
738         return breakDownItems(
739             this@FlowMeasurePolicy,
740             list.iterator(),
741             mainAxisSpacing,
742             crossAxisArrangementSpacing,
743             OrientationIndependentConstraints(
744                 constraints,
745                 if (isHorizontal) {
746                     LayoutOrientation.Horizontal
747                 } else {
748                     LayoutOrientation.Vertical
749                 }
750             ),
751             maxItemsInMainAxis,
752             maxLines,
753             overflow
754         )
755     }
756 
minIntrinsicWidthnull757     override fun IntrinsicMeasureScope.minIntrinsicWidth(
758         measurables: List<List<IntrinsicMeasurable>>,
759         height: Int
760     ): Int {
761         overflow.setOverflowMeasurables(
762             seeMoreMeasurable = measurables.getOrNull(1)?.firstOrNull(),
763             collapseMeasurable = measurables.getOrNull(2)?.firstOrNull(),
764             isHorizontal,
765             constraints = Constraints(maxHeight = height)
766         )
767         return if (isHorizontal) {
768             minIntrinsicMainAxisSize(
769                 measurables.firstOrNull() ?: listOf(),
770                 height,
771                 mainAxisSpacing.roundToPx(),
772                 crossAxisArrangementSpacing.roundToPx(),
773                 maxLines = maxLines,
774                 maxItemsInMainAxis = maxItemsInMainAxis,
775                 overflow = overflow
776             )
777         } else {
778             intrinsicCrossAxisSize(
779                 measurables.firstOrNull() ?: listOf(),
780                 height,
781                 mainAxisSpacing.roundToPx(),
782                 crossAxisArrangementSpacing.roundToPx(),
783                 maxLines = maxLines,
784                 maxItemsInMainAxis = maxItemsInMainAxis,
785                 overflow = overflow
786             )
787         }
788     }
789 
minIntrinsicHeightnull790     override fun IntrinsicMeasureScope.minIntrinsicHeight(
791         measurables: List<List<IntrinsicMeasurable>>,
792         width: Int
793     ): Int {
794         overflow.setOverflowMeasurables(
795             seeMoreMeasurable = measurables.getOrNull(1)?.firstOrNull(),
796             collapseMeasurable = measurables.getOrNull(2)?.firstOrNull(),
797             isHorizontal,
798             constraints = Constraints(maxWidth = width)
799         )
800         return if (isHorizontal) {
801             intrinsicCrossAxisSize(
802                 measurables.firstOrNull() ?: listOf(),
803                 width,
804                 mainAxisSpacing.roundToPx(),
805                 crossAxisArrangementSpacing.roundToPx(),
806                 maxLines = maxLines,
807                 maxItemsInMainAxis = maxItemsInMainAxis,
808                 overflow = overflow
809             )
810         } else {
811             minIntrinsicMainAxisSize(
812                 measurables.firstOrNull() ?: listOf(),
813                 width,
814                 mainAxisSpacing.roundToPx(),
815                 crossAxisArrangementSpacing.roundToPx(),
816                 maxLines = maxLines,
817                 maxItemsInMainAxis = maxItemsInMainAxis,
818                 overflow = overflow
819             )
820         }
821     }
822 
maxIntrinsicHeightnull823     override fun IntrinsicMeasureScope.maxIntrinsicHeight(
824         measurables: List<List<IntrinsicMeasurable>>,
825         width: Int
826     ): Int {
827         overflow.setOverflowMeasurables(
828             seeMoreMeasurable = measurables.getOrNull(1)?.firstOrNull(),
829             collapseMeasurable = measurables.getOrNull(2)?.firstOrNull(),
830             isHorizontal,
831             constraints = Constraints(maxWidth = width)
832         )
833         return if (isHorizontal) {
834             intrinsicCrossAxisSize(
835                 measurables.firstOrNull() ?: listOf(),
836                 width,
837                 mainAxisSpacing.roundToPx(),
838                 crossAxisArrangementSpacing.roundToPx(),
839                 maxLines = maxLines,
840                 maxItemsInMainAxis = maxItemsInMainAxis,
841                 overflow = overflow
842             )
843         } else {
844             maxIntrinsicMainAxisSize(
845                 measurables.firstOrNull() ?: listOf(),
846                 width,
847                 mainAxisSpacing.roundToPx(),
848             )
849         }
850     }
851 
maxIntrinsicWidthnull852     override fun IntrinsicMeasureScope.maxIntrinsicWidth(
853         measurables: List<List<IntrinsicMeasurable>>,
854         height: Int
855     ): Int {
856         overflow.setOverflowMeasurables(
857             seeMoreMeasurable = measurables.getOrNull(1)?.firstOrNull(),
858             collapseMeasurable = measurables.getOrNull(2)?.firstOrNull(),
859             isHorizontal,
860             constraints = Constraints(maxHeight = height)
861         )
862         return if (isHorizontal) {
863             maxIntrinsicMainAxisSize(
864                 measurables.firstOrNull() ?: listOf(),
865                 height,
866                 mainAxisSpacing.roundToPx(),
867             )
868         } else {
869             intrinsicCrossAxisSize(
870                 measurables.firstOrNull() ?: listOf(),
871                 height,
872                 mainAxisSpacing.roundToPx(),
873                 crossAxisArrangementSpacing.roundToPx(),
874                 maxLines = maxLines,
875                 maxItemsInMainAxis = maxItemsInMainAxis,
876                 overflow = overflow
877             )
878         }
879     }
880 
minIntrinsicMainAxisSizenull881     fun minIntrinsicMainAxisSize(
882         measurables: List<IntrinsicMeasurable>,
883         crossAxisAvailable: Int,
884         mainAxisSpacing: Int,
885         crossAxisSpacing: Int,
886         maxItemsInMainAxis: Int,
887         maxLines: Int,
888         overflow: FlowLayoutOverflowState
889     ) =
890         minIntrinsicMainAxisSize(
891             measurables,
892             mainAxisSize = { _, size -> minMainAxisIntrinsicItemSize(size) },
sizenull893             crossAxisSize = { _, size -> minCrossAxisIntrinsicItemSize(size) },
894             crossAxisAvailable,
895             mainAxisSpacing,
896             crossAxisSpacing,
897             maxItemsInMainAxis,
898             maxLines,
899             overflow
900         )
901 
maxIntrinsicMainAxisSizenull902     fun maxIntrinsicMainAxisSize(
903         measurables: List<IntrinsicMeasurable>,
904         height: Int,
905         arrangementSpacing: Int
906     ) =
907         maxIntrinsicMainAxisSize(
908             measurables,
909             { _, size -> maxMainAxisIntrinsicItemSize(size) },
910             height,
911             arrangementSpacing,
912             maxItemsInMainAxis
913         )
914 
intrinsicCrossAxisSizenull915     fun intrinsicCrossAxisSize(
916         measurables: List<IntrinsicMeasurable>,
917         mainAxisAvailable: Int,
918         mainAxisSpacing: Int,
919         crossAxisSpacing: Int,
920         maxItemsInMainAxis: Int,
921         maxLines: Int,
922         overflow: FlowLayoutOverflowState
923     ) =
924         intrinsicCrossAxisSize(
925                 measurables,
926                 mainAxisSize = { _, size -> minMainAxisIntrinsicItemSize(size) },
sizenull927                 crossAxisSize = { _, size -> minCrossAxisIntrinsicItemSize(size) },
928                 mainAxisAvailable,
929                 mainAxisSpacing,
930                 crossAxisSpacing,
931                 maxItemsInMainAxis = maxItemsInMainAxis,
932                 overflow = overflow,
933                 maxLines = maxLines
934             )
935             .first
936 
maxMainAxisIntrinsicItemSizenull937     fun IntrinsicMeasurable.maxMainAxisIntrinsicItemSize(size: Int): Int =
938         if (isHorizontal) maxIntrinsicWidth(size) else maxIntrinsicHeight(size)
939 
940     fun IntrinsicMeasurable.minCrossAxisIntrinsicItemSize(size: Int): Int =
941         if (isHorizontal) minIntrinsicHeight(size) else minIntrinsicWidth(size)
942 
943     fun IntrinsicMeasurable.minMainAxisIntrinsicItemSize(size: Int): Int =
944         if (isHorizontal) minIntrinsicWidth(size) else minIntrinsicHeight(size)
945 }
946 
947 private inline fun maxIntrinsicMainAxisSize(
948     children: List<IntrinsicMeasurable>,
949     mainAxisSize: IntrinsicMeasurable.(Int, Int) -> Int,
950     crossAxisAvailable: Int,
951     mainAxisSpacing: Int,
952     maxItemsInMainAxis: Int
953 ): Int {
954     var fixedSpace = 0
955     var currentFixedSpace = 0
956     var lastBreak = 0
957     children.fastForEachIndexed { index, child ->
958         val size = child.mainAxisSize(index, crossAxisAvailable) + mainAxisSpacing
959         if (index + 1 - lastBreak == maxItemsInMainAxis || index + 1 == children.size) {
960             lastBreak = index
961             currentFixedSpace += size
962             currentFixedSpace -= mainAxisSpacing // no mainAxisSpacing for last item in main axis
963             fixedSpace = max(fixedSpace, currentFixedSpace)
964             currentFixedSpace = 0
965         } else {
966             currentFixedSpace += size
967         }
968     }
969     return fixedSpace
970 }
971 
972 /**
973  * Slower algorithm but needed to determine the minimum main axis size Uses a binary search to
974  * search different scenarios to see the minimum main axis size
975  */
976 @Suppress("BanInlineOptIn")
977 @OptIn(ExperimentalLayoutApi::class)
minIntrinsicMainAxisSizenull978 private inline fun minIntrinsicMainAxisSize(
979     children: List<IntrinsicMeasurable>,
980     mainAxisSize: IntrinsicMeasurable.(Int, Int) -> Int,
981     crossAxisSize: IntrinsicMeasurable.(Int, Int) -> Int,
982     crossAxisAvailable: Int,
983     mainAxisSpacing: Int,
984     crossAxisSpacing: Int,
985     maxItemsInMainAxis: Int,
986     maxLines: Int,
987     overflow: FlowLayoutOverflowState
988 ): Int {
989     if (children.isEmpty()) {
990         return 0
991     }
992     val mainAxisSizes = IntArray(children.size)
993     val crossAxisSizes = IntArray(children.size)
994 
995     for (index in children.indices) {
996         val child = children[index]
997         val mainAxisItemSize = child.mainAxisSize(index, crossAxisAvailable)
998         mainAxisSizes[index] = mainAxisItemSize
999         crossAxisSizes[index] = child.crossAxisSize(index, mainAxisItemSize)
1000     }
1001 
1002     var maxItemsThatCanBeShown =
1003         if (maxLines != Int.MAX_VALUE && maxItemsInMainAxis != Int.MAX_VALUE) {
1004             maxItemsInMainAxis * maxLines
1005         } else {
1006             Int.MAX_VALUE
1007         }
1008     val mustHaveEllipsis =
1009         when {
1010             maxItemsThatCanBeShown < children.size &&
1011                 (overflow.type == FlowLayoutOverflow.OverflowType.ExpandIndicator ||
1012                     overflow.type == FlowLayoutOverflow.OverflowType.ExpandOrCollapseIndicator) ->
1013                 true
1014             maxItemsThatCanBeShown >= children.size &&
1015                 maxLines >= overflow.minLinesToShowCollapse &&
1016                 overflow.type == FlowLayoutOverflow.OverflowType.ExpandOrCollapseIndicator -> true
1017             else -> false
1018         }
1019     maxItemsThatCanBeShown -= if (mustHaveEllipsis) 1 else 0
1020     maxItemsThatCanBeShown = min(maxItemsThatCanBeShown, children.size)
1021     val maxMainAxisSize = mainAxisSizes.sum().run { this + ((children.size - 1) * mainAxisSpacing) }
1022     var mainAxisUsed = maxMainAxisSize
1023     var crossAxisUsed = crossAxisSizes.maxOf { it }
1024 
1025     val minimumItemSize = mainAxisSizes.maxOf { it }
1026     var low = minimumItemSize
1027     var high = maxMainAxisSize
1028     while (low <= high) {
1029         if (crossAxisUsed == crossAxisAvailable) {
1030             return mainAxisUsed
1031         }
1032         val mid = (low + high) / 2
1033         mainAxisUsed = mid
1034         val pair =
1035             intrinsicCrossAxisSize(
1036                 children,
1037                 mainAxisSizes,
1038                 crossAxisSizes,
1039                 mainAxisUsed,
1040                 mainAxisSpacing,
1041                 crossAxisSpacing,
1042                 maxItemsInMainAxis,
1043                 maxLines,
1044                 overflow
1045             )
1046         crossAxisUsed = pair.first
1047         val itemShown = pair.second
1048 
1049         if (crossAxisUsed > crossAxisAvailable || itemShown < maxItemsThatCanBeShown) {
1050             low = mid + 1
1051             if (low > high) {
1052                 return low
1053             }
1054         } else if (crossAxisUsed < crossAxisAvailable) {
1055             high = mid - 1
1056         } else {
1057             return mainAxisUsed
1058         }
1059     }
1060 
1061     return mainAxisUsed
1062 }
1063 
1064 /**
1065  * FlowRow: Intrinsic height (cross Axis) is based on a specified width FlowColumn: Intrinsic width
1066  * (crossAxis) based on a specified height
1067  */
intrinsicCrossAxisSizenull1068 private fun intrinsicCrossAxisSize(
1069     children: List<IntrinsicMeasurable>,
1070     mainAxisSizes: IntArray,
1071     crossAxisSizes: IntArray,
1072     mainAxisAvailable: Int,
1073     mainAxisSpacing: Int,
1074     crossAxisSpacing: Int,
1075     maxItemsInMainAxis: Int,
1076     maxLines: Int,
1077     overflow: FlowLayoutOverflowState
1078 ): IntIntPair {
1079     return intrinsicCrossAxisSize(
1080         children,
1081         { index, _ -> mainAxisSizes[index] },
1082         { index, _ -> crossAxisSizes[index] },
1083         mainAxisAvailable,
1084         mainAxisSpacing,
1085         crossAxisSpacing,
1086         maxItemsInMainAxis,
1087         maxLines,
1088         overflow
1089     )
1090 }
1091 
1092 /**
1093  * FlowRow: Intrinsic height (cross Axis) is based on a specified width
1094  * * FlowColumn: Intrinsic width (crossAxis) based on a specified height
1095  */
intrinsicCrossAxisSizenull1096 private inline fun intrinsicCrossAxisSize(
1097     children: List<IntrinsicMeasurable>,
1098     mainAxisSize: IntrinsicMeasurable.(Int, Int) -> Int,
1099     crossAxisSize: IntrinsicMeasurable.(Int, Int) -> Int,
1100     mainAxisAvailable: Int,
1101     mainAxisSpacing: Int,
1102     crossAxisSpacing: Int,
1103     maxItemsInMainAxis: Int,
1104     maxLines: Int,
1105     overflow: FlowLayoutOverflowState
1106 ): IntIntPair {
1107     if (children.isEmpty()) {
1108         return IntIntPair(0, 0)
1109     }
1110     val buildingBlocks =
1111         FlowLayoutBuildingBlocks(
1112             maxItemsInMainAxis = maxItemsInMainAxis,
1113             overflow = overflow,
1114             maxLines = maxLines,
1115             constraints =
1116                 OrientationIndependentConstraints(
1117                     mainAxisMin = 0,
1118                     mainAxisMax = mainAxisAvailable,
1119                     crossAxisMin = 0,
1120                     crossAxisMax = Constraints.Infinity
1121                 ),
1122             mainAxisSpacing = mainAxisSpacing,
1123             crossAxisSpacing = crossAxisSpacing,
1124         )
1125     var nextChild = children.getOrNull(0)
1126     var nextCrossAxisSize = nextChild?.crossAxisSize(0, mainAxisAvailable) ?: 0
1127     var nextMainAxisSize = nextChild?.mainAxisSize(0, nextCrossAxisSize) ?: 0
1128 
1129     var remaining = mainAxisAvailable
1130     var currentCrossAxisSize = 0
1131     var totalCrossAxisSize = 0
1132     var lastBreak = 0
1133     var lineIndex = 0
1134 
1135     var wrapInfo =
1136         buildingBlocks.getWrapInfo(
1137             nextItemHasNext = children.size > 1,
1138             nextIndexInLine = 0,
1139             leftOver = IntIntPair(remaining, Constraints.Infinity),
1140             nextSize =
1141                 if (nextChild == null) null else IntIntPair(nextMainAxisSize, nextCrossAxisSize),
1142             lineIndex = lineIndex,
1143             totalCrossAxisSize = totalCrossAxisSize,
1144             currentLineCrossAxisSize = currentCrossAxisSize,
1145             isWrappingRound = false,
1146             isEllipsisWrap = false
1147         )
1148 
1149     if (wrapInfo.isLastItemInContainer) {
1150         val size =
1151             overflow
1152                 .ellipsisSize(
1153                     hasNext = nextChild != null,
1154                     lineIndex = 0,
1155                     totalCrossAxisSize = 0,
1156                 )
1157                 ?.second ?: 0
1158         val noOfItemsShown = 0
1159         return IntIntPair(size, noOfItemsShown)
1160     }
1161 
1162     var noOfItemsShown = 0
1163     for (index in children.indices) {
1164         val childCrossAxisSize = nextCrossAxisSize
1165         val childMainAxisSize = nextMainAxisSize
1166         remaining -= childMainAxisSize
1167         noOfItemsShown = index + 1
1168         currentCrossAxisSize = maxOf(currentCrossAxisSize, childCrossAxisSize)
1169 
1170         // look ahead to simplify logic
1171         nextChild = children.getOrNull(index + 1)
1172         nextCrossAxisSize = nextChild?.crossAxisSize(index + 1, mainAxisAvailable) ?: 0
1173         nextMainAxisSize =
1174             nextChild?.mainAxisSize(index + 1, nextCrossAxisSize)?.plus(mainAxisSpacing) ?: 0
1175 
1176         wrapInfo =
1177             buildingBlocks.getWrapInfo(
1178                 nextItemHasNext = index + 2 < children.size,
1179                 nextIndexInLine = (index + 1) - lastBreak,
1180                 leftOver = IntIntPair(remaining, Constraints.Infinity),
1181                 nextSize =
1182                     if (nextChild == null) {
1183                         null
1184                     } else {
1185                         IntIntPair(nextMainAxisSize, nextCrossAxisSize)
1186                     },
1187                 lineIndex = lineIndex,
1188                 totalCrossAxisSize = totalCrossAxisSize,
1189                 currentLineCrossAxisSize = currentCrossAxisSize,
1190                 isWrappingRound = false,
1191                 isEllipsisWrap = false
1192             )
1193         if (wrapInfo.isLastItemInLine) {
1194             totalCrossAxisSize += currentCrossAxisSize + crossAxisSpacing
1195             val ellipsisWrapInfo =
1196                 buildingBlocks.getWrapEllipsisInfo(
1197                     wrapInfo,
1198                     hasNext = nextChild != null,
1199                     leftOverMainAxis = remaining,
1200                     lastContentLineIndex = lineIndex,
1201                     totalCrossAxisSize = totalCrossAxisSize,
1202                     nextIndexInLine = (index + 1) - lastBreak,
1203                 )
1204             currentCrossAxisSize = 0
1205             remaining = mainAxisAvailable
1206             lastBreak = index + 1
1207             nextMainAxisSize -= mainAxisSpacing
1208             lineIndex++
1209             if (wrapInfo.isLastItemInContainer) {
1210                 ellipsisWrapInfo?.ellipsisSize?.let {
1211                     if (!ellipsisWrapInfo.placeEllipsisOnLastContentLine) {
1212                         totalCrossAxisSize += it.second + crossAxisSpacing
1213                     }
1214                 }
1215                 break
1216             }
1217         }
1218     }
1219     // remove the last spacing for the last row or column
1220     totalCrossAxisSize -= crossAxisSpacing
1221     return IntIntPair(totalCrossAxisSize, noOfItemsShown)
1222 }
1223 
1224 /**
1225  * Breaks down items based on space, size and maximum items in main axis. When items run out of
1226  * space or the maximum items to fit in the main axis is reached, it moves to the next "line" and
1227  * moves the next batch of items to a new list of items
1228  */
breakDownItemsnull1229 internal fun MeasureScope.breakDownItems(
1230     measurePolicy: FlowLineMeasurePolicy,
1231     measurablesIterator: Iterator<Measurable>,
1232     mainAxisSpacingDp: Dp,
1233     crossAxisSpacingDp: Dp,
1234     constraints: OrientationIndependentConstraints,
1235     maxItemsInMainAxis: Int,
1236     maxLines: Int,
1237     overflow: FlowLayoutOverflowState,
1238 ): MeasureResult {
1239     val items = mutableVectorOf<MeasureResult>()
1240     val mainAxisMax = constraints.mainAxisMax
1241     val mainAxisMin = constraints.mainAxisMin
1242     val crossAxisMax = constraints.crossAxisMax
1243     val placeables = mutableIntObjectMapOf<Placeable?>()
1244     val measurables = mutableListOf<Measurable>()
1245 
1246     val spacing = ceil(mainAxisSpacingDp.toPx()).toInt()
1247     val crossAxisSpacing = ceil(crossAxisSpacingDp.toPx()).toInt()
1248     val subsetConstraints = OrientationIndependentConstraints(0, mainAxisMax, 0, crossAxisMax)
1249     val measureConstraints =
1250         subsetConstraints
1251             .copy(mainAxisMin = 0)
1252             .toBoxConstraints(
1253                 if (measurePolicy.isHorizontal) LayoutOrientation.Horizontal
1254                 else LayoutOrientation.Vertical
1255             )
1256 
1257     var index = 0
1258     var measurable: Measurable?
1259     var placeableItem: Placeable? = null
1260 
1261     var lineIndex = 0
1262     var leftOver = mainAxisMax
1263     var leftOverCrossAxis = crossAxisMax
1264     val lineInfo =
1265         if (measurablesIterator is ContextualFlowItemIterator) {
1266             FlowLineInfo(
1267                 lineIndex = lineIndex,
1268                 positionInLine = 0,
1269                 maxMainAxisSize = leftOver.toDp(),
1270                 maxCrossAxisSize = leftOverCrossAxis.toDp()
1271             )
1272         } else {
1273             null
1274         }
1275 
1276     var nextSize =
1277         measurablesIterator.hasNext().run {
1278             measurable = if (!this) null else measurablesIterator.safeNext(lineInfo)
1279             measurable?.measureAndCache(measurePolicy, measureConstraints) { placeable ->
1280                 placeableItem = placeable
1281             }
1282         }
1283     var nextMainAxisSize: Int? = nextSize?.first
1284     var nextCrossAxisSize: Int? = nextSize?.second
1285 
1286     var startBreakLineIndex = 0
1287     val endBreakLineList = mutableIntListOf()
1288     val crossAxisSizes = mutableIntListOf()
1289 
1290     val buildingBlocks =
1291         FlowLayoutBuildingBlocks(
1292             maxItemsInMainAxis = maxItemsInMainAxis,
1293             mainAxisSpacing = spacing,
1294             crossAxisSpacing = crossAxisSpacing,
1295             constraints = constraints,
1296             maxLines = maxLines,
1297             overflow = overflow
1298         )
1299     var ellipsisWrapInfo: FlowLayoutBuildingBlocks.WrapEllipsisInfo? = null
1300     var wrapInfo =
1301         buildingBlocks
1302             .getWrapInfo(
1303                 nextItemHasNext = measurablesIterator.hasNext(),
1304                 leftOver = IntIntPair(leftOver, leftOverCrossAxis),
1305                 totalCrossAxisSize = 0,
1306                 nextSize = nextSize,
1307                 currentLineCrossAxisSize = 0,
1308                 nextIndexInLine = 0,
1309                 isWrappingRound = false,
1310                 isEllipsisWrap = false,
1311                 lineIndex = 0
1312             )
1313             .also { wrapInfo ->
1314                 if (wrapInfo.isLastItemInContainer) {
1315                     ellipsisWrapInfo =
1316                         buildingBlocks.getWrapEllipsisInfo(
1317                             wrapInfo,
1318                             nextSize != null,
1319                             lastContentLineIndex = -1,
1320                             totalCrossAxisSize = 0,
1321                             leftOver,
1322                             nextIndexInLine = 0
1323                         )
1324                 }
1325             }
1326 
1327     // figure out the mainAxisTotalSize which will be minMainAxis when measuring the row/column
1328     var mainAxisTotalSize = mainAxisMin
1329     var crossAxisTotalSize = 0
1330     var currentLineMainAxisSize = 0
1331     var currentLineCrossAxisSize = 0
1332     while (!wrapInfo.isLastItemInContainer && measurable != null) {
1333         val itemMainAxisSize = nextMainAxisSize!!
1334         val itemCrossAxisSize = nextCrossAxisSize!!
1335         currentLineMainAxisSize += itemMainAxisSize
1336         currentLineCrossAxisSize = maxOf(currentLineCrossAxisSize, itemCrossAxisSize)
1337         leftOver -= itemMainAxisSize
1338         overflow.itemShown = index + 1
1339         measurables.add(measurable!!)
1340         placeables[index] = placeableItem
1341 
1342         val nextIndexInLine = (index + 1) - startBreakLineIndex
1343         val willFitLine = nextIndexInLine < maxItemsInMainAxis
1344 
1345         lineInfo?.update(
1346             lineIndex = if (willFitLine) lineIndex else lineIndex + 1,
1347             positionInLine = if (willFitLine) nextIndexInLine else 0,
1348             maxMainAxisSize =
1349                 if (willFitLine) {
1350                         (leftOver - spacing).fastCoerceAtLeast(0)
1351                     } else {
1352                         mainAxisMax
1353                     }
1354                     .toDp(),
1355             maxCrossAxisSize =
1356                 if (willFitLine) {
1357                         leftOverCrossAxis
1358                     } else {
1359                         (leftOverCrossAxis - currentLineCrossAxisSize - crossAxisSpacing)
1360                             .fastCoerceAtLeast(0)
1361                     }
1362                     .toDp()
1363         )
1364 
1365         nextSize =
1366             measurablesIterator.hasNext().run {
1367                 measurable = if (!this) null else measurablesIterator.safeNext(lineInfo)
1368                 placeableItem = null
1369                 measurable?.measureAndCache(measurePolicy, measureConstraints) { placeable ->
1370                     placeableItem = placeable
1371                 }
1372             }
1373         nextMainAxisSize = nextSize?.first?.plus(spacing)
1374         nextCrossAxisSize = nextSize?.second
1375 
1376         wrapInfo =
1377             buildingBlocks.getWrapInfo(
1378                 nextItemHasNext = measurablesIterator.hasNext(),
1379                 leftOver = IntIntPair(leftOver, leftOverCrossAxis),
1380                 totalCrossAxisSize = crossAxisTotalSize,
1381                 nextSize =
1382                     if (nextSize == null) null
1383                     else IntIntPair(nextMainAxisSize!!, nextCrossAxisSize!!),
1384                 currentLineCrossAxisSize = currentLineCrossAxisSize,
1385                 nextIndexInLine = nextIndexInLine,
1386                 isWrappingRound = false,
1387                 isEllipsisWrap = false,
1388                 lineIndex = lineIndex
1389             )
1390         if (wrapInfo.isLastItemInLine) {
1391             mainAxisTotalSize = maxOf(mainAxisTotalSize, currentLineMainAxisSize)
1392             mainAxisTotalSize = minOf(mainAxisTotalSize, mainAxisMax)
1393             crossAxisTotalSize += currentLineCrossAxisSize
1394             ellipsisWrapInfo =
1395                 buildingBlocks.getWrapEllipsisInfo(
1396                     wrapInfo,
1397                     nextSize != null,
1398                     lastContentLineIndex = lineIndex,
1399                     totalCrossAxisSize = crossAxisTotalSize,
1400                     leftOver,
1401                     (index + 1) - startBreakLineIndex
1402                 )
1403             crossAxisSizes.add(currentLineCrossAxisSize)
1404             leftOver = mainAxisMax
1405             leftOverCrossAxis = crossAxisMax - crossAxisTotalSize - crossAxisSpacing
1406             startBreakLineIndex = index + 1
1407             endBreakLineList.add(index + 1)
1408             currentLineMainAxisSize = 0
1409             currentLineCrossAxisSize = 0
1410             // only add spacing for next items in the row or column, not the starting indexes
1411             nextMainAxisSize = nextMainAxisSize?.minus(spacing)
1412             lineIndex++
1413             crossAxisTotalSize += crossAxisSpacing
1414         }
1415         index++
1416     }
1417 
1418     ellipsisWrapInfo?.let {
1419         measurables.add(it.ellipsis)
1420         placeables[measurables.size - 1] = it.placeable
1421         lineIndex = endBreakLineList.lastIndex
1422         if (it.placeEllipsisOnLastContentLine) {
1423             val lastIndex = endBreakLineList.size - 1
1424             val lastLineCrossAxis = crossAxisSizes[lineIndex]
1425             crossAxisSizes[lineIndex] = max(lastLineCrossAxis, it.ellipsisSize.second)
1426             endBreakLineList[lastIndex] = endBreakLineList.last() + 1
1427         } else {
1428             crossAxisSizes.add(it.ellipsisSize.second)
1429             endBreakLineList.add(endBreakLineList.last() + 1)
1430         }
1431     }
1432 
1433     val arrayOfPlaceables: Array<Placeable?> = Array(measurables.size) { placeables[it] }
1434     val crossAxisOffsets = IntArray(endBreakLineList.size)
1435     val crossAxisSizesArray = IntArray(endBreakLineList.size)
1436     crossAxisTotalSize = 0
1437 
1438     var startIndex = 0
1439     endBreakLineList.forEachIndexed { currentLineIndex, endIndex ->
1440         var crossAxisSize = crossAxisSizes[currentLineIndex]
1441         val result =
1442             measurePolicy.measure(
1443                 mainAxisMin = mainAxisTotalSize,
1444                 crossAxisMin = subsetConstraints.crossAxisMin,
1445                 mainAxisMax = subsetConstraints.mainAxisMax,
1446                 crossAxisMax = crossAxisSize,
1447                 spacing,
1448                 this,
1449                 measurables,
1450                 arrayOfPlaceables,
1451                 startIndex,
1452                 endIndex,
1453                 crossAxisOffsets,
1454                 currentLineIndex
1455             )
1456         val mainAxisSize: Int
1457         if (measurePolicy.isHorizontal) {
1458             mainAxisSize = result.width
1459             crossAxisSize = result.height
1460         } else {
1461             mainAxisSize = result.height
1462             crossAxisSize = result.width
1463         }
1464         crossAxisSizesArray[currentLineIndex] = crossAxisSize
1465         crossAxisTotalSize += crossAxisSize
1466         mainAxisTotalSize = maxOf(mainAxisTotalSize, mainAxisSize)
1467         items.add(result)
1468         startIndex = endIndex
1469     }
1470 
1471     if (items.isEmpty()) {
1472         mainAxisTotalSize = 0
1473         crossAxisTotalSize = 0
1474     }
1475 
1476     return placeHelper(
1477         constraints,
1478         mainAxisTotalSize,
1479         crossAxisTotalSize,
1480         crossAxisSizesArray,
1481         items,
1482         measurePolicy,
1483         crossAxisOffsets
1484     )
1485 }
1486 
safeNextnull1487 private fun Iterator<Measurable>.safeNext(info: FlowLineInfo?): Measurable? {
1488     return try {
1489         if (this is ContextualFlowItemIterator) {
1490             this.getNext(info!!)
1491         } else {
1492             next()
1493         }
1494     } catch (e: IndexOutOfBoundsException) {
1495         null
1496     }
1497 }
1498 
mainAxisMinnull1499 internal fun IntrinsicMeasurable.mainAxisMin(isHorizontal: Boolean, crossAxisSize: Int) =
1500     if (isHorizontal) {
1501         minIntrinsicWidth(crossAxisSize)
1502     } else {
1503         minIntrinsicHeight(crossAxisSize)
1504     }
1505 
crossAxisMinnull1506 internal fun IntrinsicMeasurable.crossAxisMin(isHorizontal: Boolean, mainAxisSize: Int) =
1507     if (isHorizontal) {
1508         minIntrinsicHeight(mainAxisSize)
1509     } else {
1510         minIntrinsicWidth(mainAxisSize)
1511     }
1512 
1513 internal val CROSS_AXIS_ALIGNMENT_TOP = CrossAxisAlignment.vertical(Alignment.Top)
1514 internal val CROSS_AXIS_ALIGNMENT_START = CrossAxisAlignment.horizontal(Alignment.Start)
1515 
1516 // We measure and cache to improve performance dramatically, instead of using intrinsics
1517 // This only works so far for fixed size items.
1518 // For weighted items, we continue to use their intrinsic widths.
1519 // This is because their fixed sizes are only determined after we determine
1520 // the number of items that can fit in the row/column it only lies on.
measureAndCachenull1521 internal fun Measurable.measureAndCache(
1522     measurePolicy: FlowLineMeasurePolicy,
1523     constraints: Constraints,
1524     storePlaceable: (Placeable?) -> Unit
1525 ): IntIntPair {
1526     return if (
1527         rowColumnParentData.weight == 0f &&
1528             rowColumnParentData?.flowLayoutData?.fillCrossAxisFraction == null
1529     ) {
1530         // fixed sizes: measure once
1531         val placeable = measure(constraints).also(storePlaceable)
1532         with(measurePolicy) {
1533             val mainAxis = placeable.mainAxisSize()
1534             val crossAxis = placeable.crossAxisSize()
1535             IntIntPair(mainAxis, crossAxis)
1536         }
1537     } else {
1538         val mainAxis = mainAxisMin(measurePolicy.isHorizontal, Constraints.Infinity)
1539         val crossAxis = crossAxisMin(measurePolicy.isHorizontal, mainAxis)
1540         IntIntPair(mainAxis, crossAxis)
1541     }
1542 }
1543 
placeHelpernull1544 internal fun MeasureScope.placeHelper(
1545     constraints: OrientationIndependentConstraints,
1546     mainAxisTotalSize: Int,
1547     crossAxisTotalSize: Int,
1548     crossAxisSizes: IntArray,
1549     items: MutableVector<MeasureResult>,
1550     measureHelper: FlowLineMeasurePolicy,
1551     outPosition: IntArray,
1552 ): MeasureResult {
1553     val isHorizontal = measureHelper.isHorizontal
1554     val verticalArrangement = measureHelper.verticalArrangement
1555     val horizontalArrangement = measureHelper.horizontalArrangement
1556     // space in between children, except for the last child
1557     var totalCrossAxisSize = crossAxisTotalSize
1558     // cross axis arrangement
1559     if (isHorizontal) {
1560         with(verticalArrangement) {
1561             val totalCrossAxisSpacing = spacing.roundToPx() * (items.size - 1)
1562             totalCrossAxisSize += totalCrossAxisSpacing
1563             totalCrossAxisSize =
1564                 totalCrossAxisSize.fastCoerceIn(constraints.crossAxisMin, constraints.crossAxisMax)
1565             arrange(totalCrossAxisSize, crossAxisSizes, outPosition)
1566         }
1567     } else {
1568         with(horizontalArrangement) {
1569             val totalCrossAxisSpacing = spacing.roundToPx() * (items.size - 1)
1570             totalCrossAxisSize += totalCrossAxisSpacing
1571             totalCrossAxisSize =
1572                 totalCrossAxisSize.fastCoerceIn(constraints.crossAxisMin, constraints.crossAxisMax)
1573             arrange(totalCrossAxisSize, crossAxisSizes, layoutDirection, outPosition)
1574         }
1575     }
1576 
1577     val finalMainAxisTotalSize =
1578         mainAxisTotalSize.fastCoerceIn(constraints.mainAxisMin, constraints.mainAxisMax)
1579 
1580     val layoutWidth: Int
1581     val layoutHeight: Int
1582     if (isHorizontal) {
1583         layoutWidth = finalMainAxisTotalSize
1584         layoutHeight = totalCrossAxisSize
1585     } else {
1586         layoutWidth = totalCrossAxisSize
1587         layoutHeight = finalMainAxisTotalSize
1588     }
1589 
1590     return layout(layoutWidth, layoutHeight) {
1591         items.forEach { measureResult -> measureResult.placeChildren() }
1592     }
1593 }
1594