1 /*
<lambda>null2  * Copyright 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.foundation.layout
18 
19 import androidx.annotation.FloatRange
20 import androidx.compose.runtime.Stable
21 import androidx.compose.ui.Alignment
22 import androidx.compose.ui.Modifier
23 import androidx.compose.ui.layout.IntrinsicMeasurable
24 import androidx.compose.ui.layout.IntrinsicMeasureScope
25 import androidx.compose.ui.layout.Measurable
26 import androidx.compose.ui.layout.MeasureResult
27 import androidx.compose.ui.layout.MeasureScope
28 import androidx.compose.ui.node.LayoutModifierNode
29 import androidx.compose.ui.node.ModifierNodeElement
30 import androidx.compose.ui.platform.InspectorInfo
31 import androidx.compose.ui.platform.debugInspectorInfo
32 import androidx.compose.ui.unit.Constraints
33 import androidx.compose.ui.unit.Density
34 import androidx.compose.ui.unit.Dp
35 import androidx.compose.ui.unit.DpSize
36 import androidx.compose.ui.unit.IntOffset
37 import androidx.compose.ui.unit.IntSize
38 import androidx.compose.ui.unit.LayoutDirection
39 import androidx.compose.ui.unit.constrain
40 import androidx.compose.ui.unit.constrainHeight
41 import androidx.compose.ui.unit.constrainWidth
42 import androidx.compose.ui.unit.isSpecified
43 import androidx.compose.ui.util.fastCoerceAtLeast
44 import androidx.compose.ui.util.fastCoerceAtMost
45 import androidx.compose.ui.util.fastCoerceIn
46 import androidx.compose.ui.util.fastRoundToInt
47 
48 /**
49  * Declare the preferred width of the content to be exactly [width]dp. The incoming measurement
50  * [Constraints] may override this value, forcing the content to be either smaller or larger.
51  *
52  * For a modifier that sets the width of the content regardless of the incoming constraints see
53  * [Modifier.requiredWidth]. See [height] or [size] to set other preferred dimensions. See
54  * [widthIn], [heightIn] or [sizeIn] to set a preferred size range.
55  *
56  * Example usage:
57  *
58  * @sample androidx.compose.foundation.layout.samples.SimpleWidthModifier
59  */
60 @Stable
61 fun Modifier.width(width: Dp) =
62     this.then(
63         SizeElement(
64             minWidth = width,
65             maxWidth = width,
66             enforceIncoming = true,
67             inspectorInfo =
68                 debugInspectorInfo {
69                     name = "width"
70                     value = width
71                 }
72         )
73     )
74 
75 /**
76  * Declare the preferred height of the content to be exactly [height]dp. The incoming measurement
77  * [Constraints] may override this value, forcing the content to be either smaller or larger.
78  *
79  * For a modifier that sets the height of the content regardless of the incoming constraints see
80  * [Modifier.requiredHeight]. See [width] or [size] to set other preferred dimensions. See
81  * [widthIn], [heightIn] or [sizeIn] to set a preferred size range.
82  *
83  * Example usage:
84  *
85  * @sample androidx.compose.foundation.layout.samples.SimpleHeightModifier
86  */
87 @Stable
Modifiernull88 fun Modifier.height(height: Dp) =
89     this.then(
90         SizeElement(
91             minHeight = height,
92             maxHeight = height,
93             enforceIncoming = true,
94             inspectorInfo =
95                 debugInspectorInfo {
96                     name = "height"
97                     value = height
98                 }
99         )
100     )
101 
102 /**
103  * Declare the preferred size of the content to be exactly [size]dp square. The incoming measurement
104  * [Constraints] may override this value, forcing the content to be either smaller or larger.
105  *
106  * For a modifier that sets the size of the content regardless of the incoming constraints, see
107  * [Modifier.requiredSize]. See [width] or [height] to set width or height alone. See [widthIn],
108  * [heightIn] or [sizeIn] to set a preferred size range.
109  *
110  * Example usage:
111  *
112  * @sample androidx.compose.foundation.layout.samples.SimpleSizeModifier
113  */
114 @Stable
sizenull115 fun Modifier.size(size: Dp) =
116     this.then(
117         SizeElement(
118             minWidth = size,
119             maxWidth = size,
120             minHeight = size,
121             maxHeight = size,
122             enforceIncoming = true,
123             inspectorInfo =
124                 debugInspectorInfo {
125                     name = "size"
126                     value = size
127                 }
128         )
129     )
130 
131 /**
132  * Declare the preferred size of the content to be exactly [width]dp by [height]dp. The incoming
133  * measurement [Constraints] may override this value, forcing the content to be either smaller or
134  * larger.
135  *
136  * For a modifier that sets the size of the content regardless of the incoming constraints, see
137  * [Modifier.requiredSize]. See [width] or [height] to set width or height alone. See [widthIn],
138  * [heightIn] or [sizeIn] to set a preferred size range.
139  *
140  * Example usage:
141  *
142  * @sample androidx.compose.foundation.layout.samples.SimpleSizeModifier
143  */
144 @Stable
sizenull145 fun Modifier.size(width: Dp, height: Dp) =
146     this.then(
147         SizeElement(
148             minWidth = width,
149             maxWidth = width,
150             minHeight = height,
151             maxHeight = height,
152             enforceIncoming = true,
153             inspectorInfo =
154                 debugInspectorInfo {
155                     name = "size"
156                     properties["width"] = width
157                     properties["height"] = height
158                 }
159         )
160     )
161 
162 /**
163  * Declare the preferred size of the content to be exactly [size]. The incoming measurement
164  * [Constraints] may override this value, forcing the content to be either smaller or larger.
165  *
166  * For a modifier that sets the size of the content regardless of the incoming constraints, see
167  * [Modifier.requiredSize]. See [width] or [height] to set width or height alone. See [widthIn],
168  * [heightIn] or [sizeIn] to set a preferred size range.
169  *
170  * Example usage:
171  *
172  * @sample androidx.compose.foundation.layout.samples.SimpleSizeModifierWithDpSize
173  */
sizenull174 @Stable fun Modifier.size(size: DpSize) = size(size.width, size.height)
175 
176 /**
177  * Constrain the width of the content to be between [min]dp and [max]dp as permitted by the incoming
178  * measurement [Constraints]. If the incoming constraints are more restrictive the requested size
179  * will obey the incoming constraints and attempt to be as close as possible to the preferred size.
180  */
181 @Stable
182 fun Modifier.widthIn(min: Dp = Dp.Unspecified, max: Dp = Dp.Unspecified) =
183     this.then(
184         SizeElement(
185             minWidth = min,
186             maxWidth = max,
187             enforceIncoming = true,
188             inspectorInfo =
189                 debugInspectorInfo {
190                     name = "widthIn"
191                     properties["min"] = min
192                     properties["max"] = max
193                 }
194         )
195     )
196 
197 /**
198  * Constrain the height of the content to be between [min]dp and [max]dp as permitted by the
199  * incoming measurement [Constraints]. If the incoming constraints are more restrictive the
200  * requested size will obey the incoming constraints and attempt to be as close as possible to the
201  * preferred size.
202  */
203 @Stable
heightInnull204 fun Modifier.heightIn(min: Dp = Dp.Unspecified, max: Dp = Dp.Unspecified) =
205     this.then(
206         SizeElement(
207             minHeight = min,
208             maxHeight = max,
209             enforceIncoming = true,
210             inspectorInfo =
211                 debugInspectorInfo {
212                     name = "heightIn"
213                     properties["min"] = min
214                     properties["max"] = max
215                 }
216         )
217     )
218 
219 /**
220  * Constrain the width of the content to be between [minWidth]dp and [maxWidth]dp and the height of
221  * the content to be between [minHeight]dp and [maxHeight]dp as permitted by the incoming
222  * measurement [Constraints]. If the incoming constraints are more restrictive the requested size
223  * will obey the incoming constraints and attempt to be as close as possible to the preferred size.
224  */
225 @Stable
sizeInnull226 fun Modifier.sizeIn(
227     minWidth: Dp = Dp.Unspecified,
228     minHeight: Dp = Dp.Unspecified,
229     maxWidth: Dp = Dp.Unspecified,
230     maxHeight: Dp = Dp.Unspecified
231 ) =
232     this.then(
233         SizeElement(
234             minWidth = minWidth,
235             minHeight = minHeight,
236             maxWidth = maxWidth,
237             maxHeight = maxHeight,
238             enforceIncoming = true,
239             inspectorInfo =
240                 debugInspectorInfo {
241                     name = "sizeIn"
242                     properties["minWidth"] = minWidth
243                     properties["minHeight"] = minHeight
244                     properties["maxWidth"] = maxWidth
245                     properties["maxHeight"] = maxHeight
246                 }
247         )
248     )
249 
250 /**
251  * Declare the width of the content to be exactly [width]dp. The incoming measurement [Constraints]
252  * will not override this value. If the content chooses a size that does not satisfy the incoming
253  * [Constraints], the parent layout will be reported a size coerced in the [Constraints], and the
254  * position of the content will be automatically offset to be centered on the space assigned to the
255  * child by the parent layout under the assumption that [Constraints] were respected.
256  *
257  * See [requiredWidthIn] and [requiredSizeIn] to set a size range. See [width] to set a preferred
258  * width, which is only respected when the incoming constraints allow it.
259  *
260  * Example usage:
261  *
262  * @sample androidx.compose.foundation.layout.samples.SimpleRequiredWidthModifier
263  */
264 @Stable
requiredWidthnull265 fun Modifier.requiredWidth(width: Dp) =
266     this.then(
267         SizeElement(
268             minWidth = width,
269             maxWidth = width,
270             enforceIncoming = false,
271             inspectorInfo =
272                 debugInspectorInfo {
273                     name = "requiredWidth"
274                     value = width
275                 }
276         )
277     )
278 
279 /**
280  * Declare the height of the content to be exactly [height]dp. The incoming measurement
281  * [Constraints] will not override this value. If the content chooses a size that does not satisfy
282  * the incoming [Constraints], the parent layout will be reported a size coerced in the
283  * [Constraints], and the position of the content will be automatically offset to be centered on the
284  * space assigned to the child by the parent layout under the assumption that [Constraints] were
285  * respected.
286  *
287  * See [requiredHeightIn] and [requiredSizeIn] to set a size range. See [height] to set a preferred
288  * height, which is only respected when the incoming constraints allow it.
289  *
290  * Example usage:
291  *
292  * @sample androidx.compose.foundation.layout.samples.SimpleRequiredHeightModifier
293  */
294 @Stable
requiredHeightnull295 fun Modifier.requiredHeight(height: Dp) =
296     this.then(
297         SizeElement(
298             minHeight = height,
299             maxHeight = height,
300             enforceIncoming = false,
301             inspectorInfo =
302                 debugInspectorInfo {
303                     name = "requiredHeight"
304                     value = height
305                 }
306         )
307     )
308 
309 /**
310  * Declare the size of the content to be exactly [size]dp width and height. The incoming measurement
311  * [Constraints] will not override this value. If the content chooses a size that does not satisfy
312  * the incoming [Constraints], the parent layout will be reported a size coerced in the
313  * [Constraints], and the position of the content will be automatically offset to be centered on the
314  * space assigned to the child by the parent layout under the assumption that [Constraints] were
315  * respected.
316  *
317  * See [requiredSizeIn] to set a size range. See [size] to set a preferred size, which is only
318  * respected when the incoming constraints allow it.
319  *
320  * Example usage:
321  *
322  * @sample androidx.compose.foundation.layout.samples.SimpleRequiredSizeModifier
323  */
324 @Stable
requiredSizenull325 fun Modifier.requiredSize(size: Dp) =
326     this.then(
327         SizeElement(
328             minWidth = size,
329             maxWidth = size,
330             minHeight = size,
331             maxHeight = size,
332             enforceIncoming = false,
333             inspectorInfo =
334                 debugInspectorInfo {
335                     name = "requiredSize"
336                     value = size
337                 }
338         )
339     )
340 
341 /**
342  * Declare the size of the content to be exactly [width]dp and [height]dp. The incoming measurement
343  * [Constraints] will not override this value. If the content chooses a size that does not satisfy
344  * the incoming [Constraints], the parent layout will be reported a size coerced in the
345  * [Constraints], and the position of the content will be automatically offset to be centered on the
346  * space assigned to the child by the parent layout under the assumption that [Constraints] were
347  * respected.
348  *
349  * See [requiredSizeIn] to set a size range. See [size] to set a preferred size, which is only
350  * respected when the incoming constraints allow it.
351  */
352 @Stable
requiredSizenull353 fun Modifier.requiredSize(width: Dp, height: Dp) =
354     this.then(
355         SizeElement(
356             minWidth = width,
357             maxWidth = width,
358             minHeight = height,
359             maxHeight = height,
360             enforceIncoming = false,
361             inspectorInfo =
362                 debugInspectorInfo {
363                     name = "requiredSize"
364                     properties["width"] = width
365                     properties["height"] = height
366                 }
367         )
368     )
369 
370 /**
371  * Declare the size of the content to be exactly [size]. The incoming measurement [Constraints] will
372  * not override this value. If the content chooses a size that does not satisfy the incoming
373  * [Constraints], the parent layout will be reported a size coerced in the [Constraints], and the
374  * position of the content will be automatically offset to be centered on the space assigned to the
375  * child by the parent layout under the assumption that [Constraints] were respected.
376  *
377  * See [requiredSizeIn] to set a size range. See [size] to set a preferred size, which is only
378  * respected when the incoming constraints allow it.
379  */
requiredSizenull380 @Stable fun Modifier.requiredSize(size: DpSize) = requiredSize(size.width, size.height)
381 
382 /**
383  * Constrain the width of the content to be between [min]dp and [max]dp. If the content chooses a
384  * size that does not satisfy the incoming [Constraints], the parent layout will be reported a size
385  * coerced in the [Constraints], and the position of the content will be automatically offset to be
386  * centered on the space assigned to the child by the parent layout under the assumption that
387  * [Constraints] were respected.
388  */
389 @Stable
390 fun Modifier.requiredWidthIn(min: Dp = Dp.Unspecified, max: Dp = Dp.Unspecified) =
391     this.then(
392         SizeElement(
393             minWidth = min,
394             maxWidth = max,
395             enforceIncoming = false,
396             inspectorInfo =
397                 debugInspectorInfo {
398                     name = "requiredWidthIn"
399                     properties["min"] = min
400                     properties["max"] = max
401                 }
402         )
403     )
404 
405 /**
406  * Constrain the height of the content to be between [min]dp and [max]dp. If the content chooses a
407  * size that does not satisfy the incoming [Constraints], the parent layout will be reported a size
408  * coerced in the [Constraints], and the position of the content will be automatically offset to be
409  * centered on the space assigned to the child by the parent layout under the assumption that
410  * [Constraints] were respected.
411  */
412 @Stable
requiredHeightInnull413 fun Modifier.requiredHeightIn(min: Dp = Dp.Unspecified, max: Dp = Dp.Unspecified) =
414     this.then(
415         SizeElement(
416             minHeight = min,
417             maxHeight = max,
418             enforceIncoming = false,
419             inspectorInfo =
420                 debugInspectorInfo {
421                     name = "requiredHeightIn"
422                     properties["min"] = min
423                     properties["max"] = max
424                 }
425         )
426     )
427 
428 /**
429  * Constrain the width of the content to be between [minWidth]dp and [maxWidth]dp, and the height of
430  * the content to be between [minHeight]dp and [maxHeight]dp. If the content chooses a size that
431  * does not satisfy the incoming [Constraints], the parent layout will be reported a size coerced in
432  * the [Constraints], and the position of the content will be automatically offset to be centered on
433  * the space assigned to the child by the parent layout under the assumption that [Constraints] were
434  * respected.
435  */
436 @Stable
requiredSizeInnull437 fun Modifier.requiredSizeIn(
438     minWidth: Dp = Dp.Unspecified,
439     minHeight: Dp = Dp.Unspecified,
440     maxWidth: Dp = Dp.Unspecified,
441     maxHeight: Dp = Dp.Unspecified
442 ) =
443     this.then(
444         SizeElement(
445             minWidth = minWidth,
446             minHeight = minHeight,
447             maxWidth = maxWidth,
448             maxHeight = maxHeight,
449             enforceIncoming = false,
450             inspectorInfo =
451                 debugInspectorInfo {
452                     name = "requiredSizeIn"
453                     properties["minWidth"] = minWidth
454                     properties["minHeight"] = minHeight
455                     properties["maxWidth"] = maxWidth
456                     properties["maxHeight"] = maxHeight
457                 }
458         )
459     )
460 
461 /**
462  * Have the content fill (possibly only partially) the [Constraints.maxWidth] of the incoming
463  * measurement constraints, by setting the [minimum width][Constraints.minWidth] and the
464  * [maximum width][Constraints.maxWidth] to be equal to the [maximum width][Constraints.maxWidth]
465  * multiplied by [fraction]. Note that, by default, the [fraction] is 1, so the modifier will make
466  * the content fill the whole available width. If the incoming maximum width is
467  * [Constraints.Infinity] this modifier will have no effect.
468  *
469  * @param fraction The fraction of the maximum width to use, between `0` and `1`, inclusive.
470  *
471  * Example usage:
472  *
473  * @sample androidx.compose.foundation.layout.samples.SimpleFillWidthModifier
474  * @sample androidx.compose.foundation.layout.samples.FillHalfWidthModifier
475  */
476 @Stable
fillMaxWidthnull477 fun Modifier.fillMaxWidth(@FloatRange(from = 0.0, to = 1.0) fraction: Float = 1f) =
478     this.then(if (fraction == 1f) FillWholeMaxWidth else FillElement.width(fraction))
479 
480 private val FillWholeMaxWidth = FillElement.width(1f)
481 
482 /**
483  * Have the content fill (possibly only partially) the [Constraints.maxHeight] of the incoming
484  * measurement constraints, by setting the [minimum height][Constraints.minHeight] and the
485  * [maximum height][Constraints.maxHeight] to be equal to the
486  * [maximum height][Constraints.maxHeight] multiplied by [fraction]. Note that, by default, the
487  * [fraction] is 1, so the modifier will make the content fill the whole available height. If the
488  * incoming maximum height is [Constraints.Infinity] this modifier will have no effect.
489  *
490  * @param fraction The fraction of the maximum height to use, between `0` and `1`, inclusive.
491  *
492  * Example usage:
493  *
494  * @sample androidx.compose.foundation.layout.samples.SimpleFillHeightModifier
495  * @sample androidx.compose.foundation.layout.samples.FillHalfHeightModifier
496  */
497 @Stable
498 fun Modifier.fillMaxHeight(@FloatRange(from = 0.0, to = 1.0) fraction: Float = 1f) =
499     this.then(if (fraction == 1f) FillWholeMaxHeight else FillElement.height(fraction))
500 
501 private val FillWholeMaxHeight = FillElement.height(1f)
502 
503 /**
504  * Have the content fill (possibly only partially) the [Constraints.maxWidth] and
505  * [Constraints.maxHeight] of the incoming measurement constraints, by setting the
506  * [minimum width][Constraints.minWidth] and the [maximum width][Constraints.maxWidth] to be equal
507  * to the [maximum width][Constraints.maxWidth] multiplied by [fraction], as well as the
508  * [minimum height][Constraints.minHeight] and the [maximum height][Constraints.minHeight] to be
509  * equal to the [maximum height][Constraints.maxHeight] multiplied by [fraction]. Note that, by
510  * default, the [fraction] is 1, so the modifier will make the content fill the whole available
511  * space. If the incoming maximum width or height is [Constraints.Infinity] this modifier will have
512  * no effect in that dimension.
513  *
514  * @param fraction The fraction of the maximum size to use, between `0` and `1`, inclusive.
515  *
516  * Example usage:
517  *
518  * @sample androidx.compose.foundation.layout.samples.SimpleFillModifier
519  * @sample androidx.compose.foundation.layout.samples.FillHalfSizeModifier
520  */
521 @Stable
522 fun Modifier.fillMaxSize(@FloatRange(from = 0.0, to = 1.0) fraction: Float = 1f) =
523     this.then(if (fraction == 1f) FillWholeMaxSize else FillElement.size(fraction))
524 
525 private val FillWholeMaxSize = FillElement.size(1f)
526 
527 /**
528  * Allow the content to measure at its desired width without regard for the incoming measurement
529  * [minimum width constraint][Constraints.minWidth], and, if [unbounded] is true, also without
530  * regard for the incoming measurement [maximum width constraint][Constraints.maxWidth]. If the
531  * content's measured size is smaller than the minimum width constraint, [align] it within that
532  * minimum width space. If the content's measured size is larger than the maximum width constraint
533  * (only possible when [unbounded] is true), [align] over the maximum width space.
534  *
535  * Example usage:
536  *
537  * @sample androidx.compose.foundation.layout.samples.SimpleWrapContentHorizontallyAlignedModifier
538  */
539 @Stable
540 fun Modifier.wrapContentWidth(
541     align: Alignment.Horizontal = Alignment.CenterHorizontally,
542     unbounded: Boolean = false
543 ) =
544     this.then(
545         if (align == Alignment.CenterHorizontally && !unbounded) {
546             WrapContentWidthCenter
547         } else if (align == Alignment.Start && !unbounded) {
548             WrapContentWidthStart
549         } else {
550             WrapContentElement.width(align, unbounded)
551         }
552     )
553 
554 private val WrapContentWidthCenter = WrapContentElement.width(Alignment.CenterHorizontally, false)
555 private val WrapContentWidthStart = WrapContentElement.width(Alignment.Start, false)
556 
557 /**
558  * Allow the content to measure at its desired height without regard for the incoming measurement
559  * [minimum height constraint][Constraints.minHeight], and, if [unbounded] is true, also without
560  * regard for the incoming measurement [maximum height constraint][Constraints.maxHeight]. If the
561  * content's measured size is smaller than the minimum height constraint, [align] it within that
562  * minimum height space. If the content's measured size is larger than the maximum height constraint
563  * (only possible when [unbounded] is true), [align] over the maximum height space.
564  *
565  * Example usage:
566  *
567  * @sample androidx.compose.foundation.layout.samples.SimpleWrapContentVerticallyAlignedModifier
568  */
569 @Stable
wrapContentHeightnull570 fun Modifier.wrapContentHeight(
571     align: Alignment.Vertical = Alignment.CenterVertically,
572     unbounded: Boolean = false
573 ) =
574     this.then(
575         if (align == Alignment.CenterVertically && !unbounded) {
576             WrapContentHeightCenter
577         } else if (align == Alignment.Top && !unbounded) {
578             WrapContentHeightTop
579         } else {
580             WrapContentElement.height(align, unbounded)
581         }
582     )
583 
584 private val WrapContentHeightCenter = WrapContentElement.height(Alignment.CenterVertically, false)
585 private val WrapContentHeightTop = WrapContentElement.height(Alignment.Top, false)
586 
587 /**
588  * Allow the content to measure at its desired size without regard for the incoming measurement
589  * [minimum width][Constraints.minWidth] or [minimum height][Constraints.minHeight] constraints,
590  * and, if [unbounded] is true, also without regard for the incoming maximum constraints. If the
591  * content's measured size is smaller than the minimum size constraint, [align] it within that
592  * minimum sized space. If the content's measured size is larger than the maximum size constraint
593  * (only possible when [unbounded] is true), [align] within the maximum space.
594  *
595  * Example usage:
596  *
597  * @sample androidx.compose.foundation.layout.samples.SimpleWrapContentAlignedModifier
598  */
599 @Stable
wrapContentSizenull600 fun Modifier.wrapContentSize(align: Alignment = Alignment.Center, unbounded: Boolean = false) =
601     this.then(
602         if (align == Alignment.Center && !unbounded) {
603             WrapContentSizeCenter
604         } else if (align == Alignment.TopStart && !unbounded) {
605             WrapContentSizeTopStart
606         } else {
607             WrapContentElement.size(align, unbounded)
608         }
609     )
610 
611 private val WrapContentSizeCenter = WrapContentElement.size(Alignment.Center, false)
612 private val WrapContentSizeTopStart = WrapContentElement.size(Alignment.TopStart, false)
613 
614 /**
615  * Constrain the size of the wrapped layout only when it would be otherwise unconstrained: the
616  * [minWidth] and [minHeight] constraints are only applied when the incoming corresponding
617  * constraint is `0`. The modifier can be used, for example, to define a default min size of a
618  * component, while still allowing it to be overidden with smaller min sizes across usages.
619  *
620  * Example usage:
621  *
622  * @sample androidx.compose.foundation.layout.samples.DefaultMinSizeSample
623  */
624 @Stable
Modifiernull625 fun Modifier.defaultMinSize(minWidth: Dp = Dp.Unspecified, minHeight: Dp = Dp.Unspecified) =
626     this.then(UnspecifiedConstraintsElement(minWidth = minWidth, minHeight = minHeight))
627 
628 private class FillElement(
629     private val direction: Direction,
630     private val fraction: Float,
631     private val inspectorName: String
632 ) : ModifierNodeElement<FillNode>() {
633     override fun create(): FillNode = FillNode(direction = direction, fraction = fraction)
634 
635     override fun update(node: FillNode) {
636         node.direction = direction
637         node.fraction = fraction
638     }
639 
640     override fun InspectorInfo.inspectableProperties() {
641         name = inspectorName
642         properties["fraction"] = fraction
643     }
644 
645     override fun equals(other: Any?): Boolean {
646         if (this === other) return true
647         if (other !is FillElement) return false
648 
649         if (direction != other.direction) return false
650         if (fraction != other.fraction) return false
651 
652         return true
653     }
654 
655     override fun hashCode(): Int {
656         var result = direction.hashCode()
657         result = 31 * result + fraction.hashCode()
658         return result
659     }
660 
661     @Suppress("ModifierFactoryExtensionFunction", "ModifierFactoryReturnType")
662     companion object {
663         @Stable
664         fun width(fraction: Float) =
665             FillElement(
666                 direction = Direction.Horizontal,
667                 fraction = fraction,
668                 inspectorName = "fillMaxWidth"
669             )
670 
671         @Stable
672         fun height(fraction: Float) =
673             FillElement(
674                 direction = Direction.Vertical,
675                 fraction = fraction,
676                 inspectorName = "fillMaxHeight"
677             )
678 
679         @Stable
680         fun size(fraction: Float) =
681             FillElement(
682                 direction = Direction.Both,
683                 fraction = fraction,
684                 inspectorName = "fillMaxSize"
685             )
686     }
687 }
688 
689 private class FillNode(var direction: Direction, var fraction: Float) :
690     LayoutModifierNode, Modifier.Node() {
measurenull691     override fun MeasureScope.measure(
692         measurable: Measurable,
693         constraints: Constraints
694     ): MeasureResult {
695         val minWidth: Int
696         val maxWidth: Int
697         if (constraints.hasBoundedWidth && direction != Direction.Vertical) {
698             val width =
699                 (constraints.maxWidth * fraction)
700                     .fastRoundToInt()
701                     .fastCoerceIn(constraints.minWidth, constraints.maxWidth)
702             minWidth = width
703             maxWidth = width
704         } else {
705             minWidth = constraints.minWidth
706             maxWidth = constraints.maxWidth
707         }
708         val minHeight: Int
709         val maxHeight: Int
710         if (constraints.hasBoundedHeight && direction != Direction.Horizontal) {
711             val height =
712                 (constraints.maxHeight * fraction)
713                     .fastRoundToInt()
714                     .fastCoerceIn(constraints.minHeight, constraints.maxHeight)
715             minHeight = height
716             maxHeight = height
717         } else {
718             minHeight = constraints.minHeight
719             maxHeight = constraints.maxHeight
720         }
721         val placeable = measurable.measure(Constraints(minWidth, maxWidth, minHeight, maxHeight))
722 
723         return layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) }
724     }
725 }
726 
727 private class SizeElement(
728     private val minWidth: Dp = Dp.Unspecified,
729     private val minHeight: Dp = Dp.Unspecified,
730     private val maxWidth: Dp = Dp.Unspecified,
731     private val maxHeight: Dp = Dp.Unspecified,
732     private val enforceIncoming: Boolean,
733     private val inspectorInfo: InspectorInfo.() -> Unit
734 ) : ModifierNodeElement<SizeNode>() {
createnull735     override fun create(): SizeNode =
736         SizeNode(
737             minWidth = minWidth,
738             minHeight = minHeight,
739             maxWidth = maxWidth,
740             maxHeight = maxHeight,
741             enforceIncoming = enforceIncoming
742         )
743 
744     override fun update(node: SizeNode) {
745         node.minWidth = minWidth
746         node.minHeight = minHeight
747         node.maxWidth = maxWidth
748         node.maxHeight = maxHeight
749         node.enforceIncoming = enforceIncoming
750     }
751 
inspectablePropertiesnull752     override fun InspectorInfo.inspectableProperties() {
753         inspectorInfo()
754     }
755 
equalsnull756     override fun equals(other: Any?): Boolean {
757         if (this === other) return true
758         if (other !is SizeElement) return false
759 
760         if (minWidth != other.minWidth) return false
761         if (minHeight != other.minHeight) return false
762         if (maxWidth != other.maxWidth) return false
763         if (maxHeight != other.maxHeight) return false
764         if (enforceIncoming != other.enforceIncoming) return false
765 
766         return true
767     }
768 
hashCodenull769     override fun hashCode(): Int {
770         var result = minWidth.hashCode()
771         result = 31 * result + minHeight.hashCode()
772         result = 31 * result + maxWidth.hashCode()
773         result = 31 * result + maxHeight.hashCode()
774         result = 31 * result + enforceIncoming.hashCode()
775         return result
776     }
777 }
778 
779 private class SizeNode(
780     var minWidth: Dp = Dp.Unspecified,
781     var minHeight: Dp = Dp.Unspecified,
782     var maxWidth: Dp = Dp.Unspecified,
783     var maxHeight: Dp = Dp.Unspecified,
784     var enforceIncoming: Boolean
785 ) : LayoutModifierNode, Modifier.Node() {
786     private val Density.targetConstraints: Constraints
787         get() {
788             val maxWidth =
789                 if (maxWidth.isSpecified) {
790                     maxWidth.roundToPx().fastCoerceAtLeast(0)
791                 } else {
792                     Constraints.Infinity
793                 }
794             val maxHeight =
795                 if (maxHeight.isSpecified) {
796                     maxHeight.roundToPx().fastCoerceAtLeast(0)
797                 } else {
798                     Constraints.Infinity
799                 }
800             val minWidth =
801                 if (minWidth.isSpecified) {
<lambda>null802                     minWidth.roundToPx().fastCoerceIn(0, maxWidth).let {
803                         if (it != Constraints.Infinity) it else 0
804                     }
805                 } else {
806                     0
807                 }
808             val minHeight =
809                 if (minHeight.isSpecified) {
<lambda>null810                     minHeight.roundToPx().fastCoerceIn(0, maxHeight).let {
811                         if (it != Constraints.Infinity) it else 0
812                     }
813                 } else {
814                     0
815                 }
816             return Constraints(
817                 minWidth = minWidth,
818                 minHeight = minHeight,
819                 maxWidth = maxWidth,
820                 maxHeight = maxHeight
821             )
822         }
823 
measurenull824     override fun MeasureScope.measure(
825         measurable: Measurable,
826         constraints: Constraints
827     ): MeasureResult {
828         val wrappedConstraints =
829             targetConstraints.let { targetConstraints ->
830                 if (enforceIncoming) {
831                     constraints.constrain(targetConstraints)
832                 } else {
833                     val resolvedMinWidth =
834                         if (minWidth.isSpecified) {
835                             targetConstraints.minWidth
836                         } else {
837                             constraints.minWidth.fastCoerceAtMost(targetConstraints.maxWidth)
838                         }
839                     val resolvedMaxWidth =
840                         if (maxWidth.isSpecified) {
841                             targetConstraints.maxWidth
842                         } else {
843                             constraints.maxWidth.fastCoerceAtLeast(targetConstraints.minWidth)
844                         }
845                     val resolvedMinHeight =
846                         if (minHeight.isSpecified) {
847                             targetConstraints.minHeight
848                         } else {
849                             constraints.minHeight.fastCoerceAtMost(targetConstraints.maxHeight)
850                         }
851                     val resolvedMaxHeight =
852                         if (maxHeight.isSpecified) {
853                             targetConstraints.maxHeight
854                         } else {
855                             constraints.maxHeight.fastCoerceAtLeast(targetConstraints.minHeight)
856                         }
857                     Constraints(
858                         resolvedMinWidth,
859                         resolvedMaxWidth,
860                         resolvedMinHeight,
861                         resolvedMaxHeight
862                     )
863                 }
864             }
865         val placeable = measurable.measure(wrappedConstraints)
866         return layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) }
867     }
868 
minIntrinsicWidthnull869     override fun IntrinsicMeasureScope.minIntrinsicWidth(
870         measurable: IntrinsicMeasurable,
871         height: Int
872     ): Int {
873         val constraints = targetConstraints
874         return if (constraints.hasFixedWidth) {
875             constraints.maxWidth
876         } else {
877             val childHeight = if (enforceIncoming) height else constraints.constrainHeight(height)
878             constraints.constrainWidth(measurable.minIntrinsicWidth(childHeight))
879         }
880     }
881 
minIntrinsicHeightnull882     override fun IntrinsicMeasureScope.minIntrinsicHeight(
883         measurable: IntrinsicMeasurable,
884         width: Int
885     ): Int {
886         val constraints = targetConstraints
887         return if (constraints.hasFixedHeight) {
888             constraints.maxHeight
889         } else {
890             val childWidth = if (enforceIncoming) width else constraints.constrainWidth(width)
891             constraints.constrainHeight(measurable.minIntrinsicHeight(childWidth))
892         }
893     }
894 
maxIntrinsicWidthnull895     override fun IntrinsicMeasureScope.maxIntrinsicWidth(
896         measurable: IntrinsicMeasurable,
897         height: Int
898     ): Int {
899         val constraints = targetConstraints
900         return if (constraints.hasFixedWidth) {
901             constraints.maxWidth
902         } else {
903             val childHeight = if (enforceIncoming) height else constraints.constrainHeight(height)
904             constraints.constrainWidth(measurable.maxIntrinsicWidth(childHeight))
905         }
906     }
907 
maxIntrinsicHeightnull908     override fun IntrinsicMeasureScope.maxIntrinsicHeight(
909         measurable: IntrinsicMeasurable,
910         width: Int
911     ): Int {
912         val constraints = targetConstraints
913         return if (constraints.hasFixedHeight) {
914             constraints.maxHeight
915         } else {
916             val childWidth = if (enforceIncoming) width else constraints.constrainWidth(width)
917             constraints.constrainHeight(measurable.maxIntrinsicHeight(childWidth))
918         }
919     }
920 }
921 
922 private class WrapContentElement(
923     private val direction: Direction,
924     private val unbounded: Boolean,
925     private val alignmentCallback: (IntSize, LayoutDirection) -> IntOffset,
926     private val align: Any, // only used for equals and hashcode
927     private val inspectorName: String
928 ) : ModifierNodeElement<WrapContentNode>() {
createnull929     override fun create(): WrapContentNode =
930         WrapContentNode(direction, unbounded, alignmentCallback)
931 
932     override fun update(node: WrapContentNode) {
933         node.direction = direction
934         node.unbounded = unbounded
935         node.alignmentCallback = alignmentCallback
936     }
937 
inspectablePropertiesnull938     override fun InspectorInfo.inspectableProperties() {
939         name = inspectorName
940         properties["align"] = align
941         properties["unbounded"] = unbounded
942     }
943 
equalsnull944     override fun equals(other: Any?): Boolean {
945         if (this === other) return true
946         if (other === null) return false
947         if (this::class != other::class) return false
948 
949         other as WrapContentElement
950 
951         if (direction != other.direction) return false
952         if (unbounded != other.unbounded) return false
953         if (align != other.align) return false
954 
955         return true
956     }
957 
hashCodenull958     override fun hashCode(): Int {
959         var result = direction.hashCode()
960         result = 31 * result + unbounded.hashCode()
961         result = 31 * result + align.hashCode()
962         return result
963     }
964 
965     @Suppress("ModifierFactoryExtensionFunction", "ModifierFactoryReturnType")
966     companion object {
967         @Stable
widthnull968         fun width(align: Alignment.Horizontal, unbounded: Boolean) =
969             WrapContentElement(
970                 direction = Direction.Horizontal,
971                 unbounded = unbounded,
972                 alignmentCallback = { size, layoutDirection ->
973                     IntOffset(align.align(0, size.width, layoutDirection), 0)
974                 },
975                 align,
976                 inspectorName = "wrapContentWidth"
977             )
978 
979         @Stable
heightnull980         fun height(align: Alignment.Vertical, unbounded: Boolean) =
981             WrapContentElement(
982                 direction = Direction.Vertical,
983                 unbounded = unbounded,
984                 alignmentCallback = { size, _ -> IntOffset(0, align.align(0, size.height)) },
985                 align,
986                 inspectorName = "wrapContentHeight"
987             )
988 
989         @Stable
sizenull990         fun size(align: Alignment, unbounded: Boolean) =
991             WrapContentElement(
992                 direction = Direction.Both,
993                 unbounded = unbounded,
994                 alignmentCallback = { size, layoutDirection ->
995                     align.align(IntSize.Zero, size, layoutDirection)
996                 },
997                 align,
998                 inspectorName = "wrapContentSize"
999             )
1000     }
1001 }
1002 
1003 private class WrapContentNode(
1004     var direction: Direction,
1005     var unbounded: Boolean,
1006     var alignmentCallback: (IntSize, LayoutDirection) -> IntOffset,
1007 ) : LayoutModifierNode, Modifier.Node() {
measurenull1008     override fun MeasureScope.measure(
1009         measurable: Measurable,
1010         constraints: Constraints
1011     ): MeasureResult {
1012         val wrappedConstraints =
1013             Constraints(
1014                 minWidth = if (direction != Direction.Vertical) 0 else constraints.minWidth,
1015                 minHeight = if (direction != Direction.Horizontal) 0 else constraints.minHeight,
1016                 maxWidth =
1017                     if (direction != Direction.Vertical && unbounded) {
1018                         Constraints.Infinity
1019                     } else {
1020                         constraints.maxWidth
1021                     },
1022                 maxHeight =
1023                     if (direction != Direction.Horizontal && unbounded) {
1024                         Constraints.Infinity
1025                     } else {
1026                         constraints.maxHeight
1027                     }
1028             )
1029         val placeable = measurable.measure(wrappedConstraints)
1030         val wrapperWidth = placeable.width.coerceIn(constraints.minWidth, constraints.maxWidth)
1031         val wrapperHeight = placeable.height.coerceIn(constraints.minHeight, constraints.maxHeight)
1032         return layout(wrapperWidth, wrapperHeight) {
1033             val position =
1034                 alignmentCallback(
1035                     IntSize(wrapperWidth - placeable.width, wrapperHeight - placeable.height),
1036                     layoutDirection
1037                 )
1038             placeable.place(position)
1039         }
1040     }
1041 }
1042 
1043 private class UnspecifiedConstraintsElement(
1044     val minWidth: Dp = Dp.Unspecified,
1045     val minHeight: Dp = Dp.Unspecified,
1046 ) : ModifierNodeElement<UnspecifiedConstraintsNode>() {
createnull1047     override fun create(): UnspecifiedConstraintsNode =
1048         UnspecifiedConstraintsNode(minWidth = minWidth, minHeight = minHeight)
1049 
1050     override fun update(node: UnspecifiedConstraintsNode) {
1051         node.minWidth = minWidth
1052         node.minHeight = minHeight
1053     }
1054 
inspectablePropertiesnull1055     override fun InspectorInfo.inspectableProperties() {
1056         name = "defaultMinSize"
1057         properties["minWidth"] = minWidth
1058         properties["minHeight"] = minHeight
1059     }
1060 
equalsnull1061     override fun equals(other: Any?): Boolean {
1062         if (other !is UnspecifiedConstraintsElement) return false
1063         return minWidth == other.minWidth && minHeight == other.minHeight
1064     }
1065 
hashCodenull1066     override fun hashCode() = minWidth.hashCode() * 31 + minHeight.hashCode()
1067 }
1068 
1069 private class UnspecifiedConstraintsNode(
1070     var minWidth: Dp = Dp.Unspecified,
1071     var minHeight: Dp = Dp.Unspecified
1072 ) : LayoutModifierNode, Modifier.Node() {
1073     override fun MeasureScope.measure(
1074         measurable: Measurable,
1075         constraints: Constraints
1076     ): MeasureResult {
1077         val wrappedConstraints =
1078             Constraints(
1079                 if (minWidth.isSpecified && constraints.minWidth == 0) {
1080                     minWidth.roundToPx().fastCoerceIn(0, constraints.maxWidth)
1081                 } else {
1082                     constraints.minWidth
1083                 },
1084                 constraints.maxWidth,
1085                 if (minHeight.isSpecified && constraints.minHeight == 0) {
1086                     minHeight.roundToPx().fastCoerceIn(0, constraints.maxHeight)
1087                 } else {
1088                     constraints.minHeight
1089                 },
1090                 constraints.maxHeight
1091             )
1092         val placeable = measurable.measure(wrappedConstraints)
1093         return layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) }
1094     }
1095 
1096     override fun IntrinsicMeasureScope.minIntrinsicWidth(
1097         measurable: IntrinsicMeasurable,
1098         height: Int
1099     ) =
1100         measurable
1101             .minIntrinsicWidth(height)
1102             .fastCoerceAtLeast(if (minWidth.isSpecified) minWidth.roundToPx() else 0)
1103 
1104     override fun IntrinsicMeasureScope.maxIntrinsicWidth(
1105         measurable: IntrinsicMeasurable,
1106         height: Int
1107     ) =
1108         measurable
1109             .maxIntrinsicWidth(height)
1110             .fastCoerceAtLeast(if (minWidth.isSpecified) minWidth.roundToPx() else 0)
1111 
1112     override fun IntrinsicMeasureScope.minIntrinsicHeight(
1113         measurable: IntrinsicMeasurable,
1114         width: Int
1115     ) =
1116         measurable
1117             .minIntrinsicHeight(width)
1118             .fastCoerceAtLeast(if (minHeight.isSpecified) minHeight.roundToPx() else 0)
1119 
1120     override fun IntrinsicMeasureScope.maxIntrinsicHeight(
1121         measurable: IntrinsicMeasurable,
1122         width: Int
1123     ) =
1124         measurable
1125             .maxIntrinsicHeight(width)
1126             .fastCoerceAtLeast(if (minHeight.isSpecified) minHeight.roundToPx() else 0)
1127 }
1128 
1129 internal enum class Direction {
1130     Vertical,
1131     Horizontal,
1132     Both
1133 }
1134