1 /*
2  * Copyright 2024 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 package androidx.compose.foundation.layout
17 
18 import androidx.collection.IntIntPair
19 import androidx.compose.ui.layout.Measurable
20 import androidx.compose.ui.layout.Placeable
21 import kotlin.math.max
22 
23 @OptIn(ExperimentalLayoutApi::class)
24 internal class FlowLayoutBuildingBlocks(
25     private val maxItemsInMainAxis: Int,
26     private val overflow: FlowLayoutOverflowState,
27     private val constraints: OrientationIndependentConstraints,
28     private val maxLines: Int,
29     private val mainAxisSpacing: Int,
30     private val crossAxisSpacing: Int,
31 ) {
32     class WrapInfo(
33         val isLastItemInLine: Boolean = false,
34         val isLastItemInContainer: Boolean = false
35     )
36 
37     class WrapEllipsisInfo(
38         val ellipsis: Measurable,
39         val placeable: Placeable?,
40         val ellipsisSize: IntIntPair,
41         var placeEllipsisOnLastContentLine: Boolean = true,
42     )
43 
getWrapEllipsisInfonull44     fun getWrapEllipsisInfo(
45         wrapInfo: WrapInfo,
46         hasNext: Boolean,
47         lastContentLineIndex: Int,
48         totalCrossAxisSize: Int,
49         leftOverMainAxis: Int,
50         nextIndexInLine: Int
51     ): WrapEllipsisInfo? {
52         if (!wrapInfo.isLastItemInContainer) return null
53 
54         val ellipsisInfo =
55             overflow.ellipsisInfo(hasNext, lastContentLineIndex, totalCrossAxisSize) ?: return null
56 
57         val canFitLine =
58             lastContentLineIndex >= 0 &&
59                 (nextIndexInLine == 0 ||
60                     !(leftOverMainAxis - ellipsisInfo.ellipsisSize.first < 0 ||
61                         nextIndexInLine >= maxItemsInMainAxis))
62 
63         ellipsisInfo.placeEllipsisOnLastContentLine = canFitLine
64         return ellipsisInfo
65     }
66 
67     @Suppress("DEPRECATION")
getWrapInfonull68     fun getWrapInfo(
69         nextItemHasNext: Boolean,
70         nextIndexInLine: Int,
71         leftOver: IntIntPair,
72         nextSize: IntIntPair?,
73         lineIndex: Int,
74         totalCrossAxisSize: Int,
75         currentLineCrossAxisSize: Int,
76         isWrappingRound: Boolean,
77         isEllipsisWrap: Boolean,
78     ): WrapInfo {
79         var totalContainerCrossAxisSize = totalCrossAxisSize + currentLineCrossAxisSize
80         if (nextSize == null) {
81             return WrapInfo(isLastItemInLine = true, isLastItemInContainer = true)
82         }
83 
84         val willOverflowCrossAxis =
85             when {
86                 overflow.type == FlowLayoutOverflow.OverflowType.Visible -> false
87                 lineIndex >= maxLines -> true
88                 leftOver.second - nextSize.second < 0 -> true
89                 else -> false
90             }
91 
92         if (willOverflowCrossAxis) {
93             return WrapInfo(isLastItemInLine = true, isLastItemInContainer = true)
94         }
95 
96         val shouldWrapItem =
97             when {
98                 nextIndexInLine == 0 -> false
99                 nextIndexInLine >= maxItemsInMainAxis -> true
100                 leftOver.first - nextSize.first < 0 -> true
101                 else -> false
102             }
103 
104         if (shouldWrapItem) {
105             if (isWrappingRound) {
106                 return WrapInfo(isLastItemInLine = true, isLastItemInContainer = true)
107             }
108             val wrapInfo =
109                 getWrapInfo(
110                     nextItemHasNext,
111                     nextIndexInLine = 0,
112                     leftOver =
113                         IntIntPair(
114                             constraints.mainAxisMax,
115                             leftOver.second - crossAxisSpacing - currentLineCrossAxisSize
116                         ),
117                     // remove the mainAxisSpacing added to 2nd position or more indexed items.
118                     IntIntPair(
119                         first = nextSize.first.minus(mainAxisSpacing),
120                         second = nextSize.second
121                     ),
122                     lineIndex = lineIndex + 1,
123                     totalCrossAxisSize = totalContainerCrossAxisSize,
124                     currentLineCrossAxisSize = 0,
125                     isWrappingRound = true,
126                     isEllipsisWrap = false
127                 )
128             return WrapInfo(
129                 isLastItemInLine = true,
130                 isLastItemInContainer = wrapInfo.isLastItemInContainer
131             )
132         }
133 
134         totalContainerCrossAxisSize =
135             totalCrossAxisSize + max(currentLineCrossAxisSize, nextSize.second)
136 
137         val ellipsis =
138             if (isEllipsisWrap) {
139                 null
140             } else {
141                 overflow.ellipsisSize(nextItemHasNext, lineIndex, totalContainerCrossAxisSize)
142             }
143         val shouldWrapEllipsis =
144             ellipsis?.run {
145                 when {
146                     nextIndexInLine + 1 >= maxItemsInMainAxis -> true
147                     leftOver.first - nextSize.first - mainAxisSpacing - ellipsis.first < 0 -> true
148                     else -> false
149                 }
150             } ?: false
151 
152         if (shouldWrapEllipsis) {
153             if (isEllipsisWrap) {
154                 return WrapInfo(isLastItemInLine = true, isLastItemInContainer = true)
155             }
156             val wrapInfo =
157                 getWrapInfo(
158                     nextItemHasNext = false,
159                     nextIndexInLine = 0,
160                     IntIntPair(
161                         constraints.mainAxisMax,
162                         leftOver.second -
163                             crossAxisSpacing -
164                             max(currentLineCrossAxisSize, nextSize.second)
165                     ),
166                     ellipsis,
167                     lineIndex = lineIndex + 1,
168                     totalCrossAxisSize = totalContainerCrossAxisSize,
169                     currentLineCrossAxisSize = 0,
170                     isWrappingRound = true,
171                     isEllipsisWrap = true
172                 )
173 
174             return WrapInfo(
175                 isLastItemInLine = wrapInfo.isLastItemInContainer,
176                 isLastItemInContainer = wrapInfo.isLastItemInContainer
177             )
178         }
179 
180         return WrapInfo(isLastItemInLine = false, isLastItemInContainer = false)
181     }
182 }
183