Constraints.kt

/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
@file:Suppress(
    "NOTHING_TO_INLINE",
    "INLINE_CLASS_DEPRECATED",
    "EXPERIMENTAL_FEATURE_WARNING"
)

package androidx.compose.ui.unit

import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable

/**
 * Immutable constraints for measuring layouts, used by [layouts][androidx.compose.ui.layout.Layout]
 * or [layout modifiers][androidx.compose.ui.layout.LayoutModifier] to measure their layout
 * children. The parent chooses the [Constraints] defining a range, in pixels, within which
 * the measured layout should choose a size:
 *
 * - `minWidth` <= `chosenWidth` <= `maxWidth`
 * - `minHeight` <= `chosenHeight` <= `maxHeight`
 *
 * For more details about how layout measurement works, see
 * [androidx.compose.ui.layout.MeasurePolicy] or
 * [androidx.compose.ui.layout.LayoutModifier.measure].
 *
 * A set of [Constraints] can have infinite maxWidth and/or maxHeight. This is a trick often
 * used by parents to ask their children for their preferred size: unbounded constraints force
 * children whose default behavior is to fill the available space (always size to
 * maxWidth/maxHeight) to have an opinion about their preferred size. Most commonly, when measured
 * with unbounded [Constraints], these children will fallback to size themselves to wrap their
 * content, instead of expanding to fill the available space (this is not always true
 * as it depends on the child layout model, but is a common behavior for core layout components).
 *
 * [Constraints] uses a [Long] to represent four values, [minWidth], [minHeight], [maxWidth],
 * and [maxHeight]. The range of the values varies to allow for at most 256K in one dimension.
 * There are four possible maximum ranges, 13 bits/18 bits, and 15 bits/16 bits for either width
 * or height, depending on the needs. For example, a width could range up to 18 bits
 * and the height up to 13 bits. Alternatively, the width could range up to 16 bits and the height
 * up to 15 bits. The height and width requirements can be reversed, with a height of up to 18 bits
 * and width of 13 bits or height of 16 bits and width of 15 bits. Any constraints exceeding
 * this range will fail.
 */
@Immutable
// This can be made inline after b/155690960 is fixed.
inline class Constraints(
    @PublishedApi internal val value: Long
) {
    /**
     * Indicates how the bits are assigned. One of:
     * * MinFocusWidth
     * * MaxFocusWidth
     * * MinFocusHeight
     * * MaxFocusHeight
     */
    private val focusIndex
        get() = (value and FocusMask).toInt()

    /**
     * The minimum width that the measurement can take, in pixels.
     */
    val minWidth: Int
        get() {
            val mask = WidthMask[focusIndex]
            return ((value shr 2).toInt() and mask)
        }

    /**
     * The maximum width that the measurement can take, in pixels. This will either be
     * a positive value greater than or equal to [minWidth] or [Constraints.Infinity].
     */
    val maxWidth: Int
        get() {
            val mask = WidthMask[focusIndex]
            val width = ((value shr 33).toInt() and mask)
            return if (width == 0) Infinity else width - 1
        }

    /**
     * The minimum height that the measurement can take, in pixels.
     */
    val minHeight: Int
        get() {
            val focus = focusIndex
            val mask = HeightMask[focus]
            val offset = MinHeightOffsets[focus]
            return (value shr offset).toInt() and mask
        }

    /**
     * The maximum height that the measurement can take, in pixels. This will either be
     * a positive value greater than or equal to [minHeight] or [Constraints.Infinity].
     */
    val maxHeight: Int
        get() {
            val focus = focusIndex
            val mask = HeightMask[focus]
            val offset = MinHeightOffsets[focus] + 31
            val height = (value shr offset).toInt() and mask
            return if (height == 0) Infinity else height - 1
        }

    /**
     * `false` when [maxWidth] is [Infinity] and `true` if [maxWidth] is a non-[Infinity] value.
     * @see hasBoundedHeight
     */
    val hasBoundedWidth: Boolean
        get() {
            val mask = WidthMask[focusIndex]
            return ((value shr 33).toInt() and mask) != 0
        }

    /**
     * `false` when [maxHeight] is [Infinity] and `true` if [maxHeight] is a non-[Infinity] value.
     * @see hasBoundedWidth
     */
    val hasBoundedHeight: Boolean
        get() {
            val focus = focusIndex
            val mask = HeightMask[focus]
            val offset = MinHeightOffsets[focus] + 31
            return ((value shr offset).toInt() and mask) != 0
        }

    /**
     * Whether there is exactly one width value that satisfies the constraints.
     */
    @Stable
    val hasFixedWidth get() = maxWidth == minWidth

    /**
     * Whether there is exactly one height value that satisfies the constraints.
     */
    @Stable
    val hasFixedHeight get() = maxHeight == minHeight

    /**
     * Whether the area of a component respecting these constraints will definitely be 0.
     * This is true when at least one of maxWidth and maxHeight are 0.
     */
    @Stable
    val isZero get() = maxWidth == 0 || maxHeight == 0

    /**
     * Copies the existing [Constraints], replacing some of [minWidth], [minHeight], [maxWidth],
     * or [maxHeight] as desired. [minWidth] and [minHeight] must be positive and
     * [maxWidth] and [maxHeight] must be greater than or equal to [minWidth] and [minHeight],
     * respectively, or [Infinity].
     */
    fun copy(
        minWidth: Int = this.minWidth,
        maxWidth: Int = this.maxWidth,
        minHeight: Int = this.minHeight,
        maxHeight: Int = this.maxHeight
    ): Constraints {
        require(minHeight >= 0 && minWidth >= 0) {
            "minHeight($minHeight) and minWidth($minWidth) must be >= 0"
        }
        require(maxWidth >= minWidth || maxWidth == Infinity) {
            "maxWidth($maxWidth) must be >= minWidth($minWidth)"
        }
        require(maxHeight >= minHeight || maxHeight == Infinity) {
            "maxHeight($maxHeight) must be >= minHeight($minHeight)"
        }
        return createConstraints(minWidth, maxWidth, minHeight, maxHeight)
    }

    override fun toString(): String {
        val maxWidth = maxWidth
        val maxWidthStr = if (maxWidth == Infinity) "Infinity" else maxWidth.toString()
        val maxHeight = maxHeight
        val maxHeightStr = if (maxHeight == Infinity) "Infinity" else maxHeight.toString()
        return "Constraints(minWidth = $minWidth, maxWidth = $maxWidthStr, " +
            "minHeight = $minHeight, maxHeight = $maxHeightStr)"
    }

    companion object {
        /**
         * A value that [maxWidth] or [maxHeight] will be set to when the constraint should
         * be considered infinite. [hasBoundedWidth] or [hasBoundedHeight] will be
         * `false` when [maxWidth] or [maxHeight] is [Infinity], respectively.
         */
        const val Infinity = Int.MAX_VALUE

        /**
         * The bit distribution when the focus of the bits should be on the width, but only
         * a minimal difference in focus.
         *
         * 16 bits assigned to width, 15 bits assigned to height.
         */
        private const val MinFocusWidth = 0x00L

        /**
         * The bit distribution when the focus of the bits should be on the width, and a
         * maximal number of bits assigned to the width.
         *
         * 18 bits assigned to width, 13 bits assigned to height.
         */
        private const val MaxFocusWidth = 0x01L

        /**
         * The bit distribution when the focus of the bits should be on the height, but only
         * a minimal difference in focus.
         *
         * 15 bits assigned to width, 16 bits assigned to height.
         */
        private const val MinFocusHeight = 0x02L

        /**
         * The bit distribution when the focus of the bits should be on the height, and a
         * a maximal number of bits assigned to the height.
         *
         * 13 bits assigned to width, 18 bits assigned to height.
         */
        private const val MaxFocusHeight = 0x03L

        /**
         * The mask to retrieve the focus ([MinFocusWidth], [MaxFocusWidth],
         * [MinFocusHeight], [MaxFocusHeight]).
         */
        private const val FocusMask = 0x03L

        /**
         * The number of bits used for the focused dimension when there is minimal focus.
         */
        private const val MinFocusBits = 16

        /**
         * The mask to use for the focused dimension when there is minimal focus.
         */
        private const val MinFocusMask = 0xFFFF // 64K (16 bits)

        /**
         * The number of bits used for the non-focused dimension when there is minimal focus.
         */
        private const val MinNonFocusBits = 15

        /**
         * The mask to use for the non-focused dimension when there is minimal focus.
         */
        private const val MinNonFocusMask = 0x7FFF // 32K (15 bits)

        /**
         * The number of bits to use for the focused dimension when there is maximal focus.
         */
        private const val MaxFocusBits = 18

        /**
         * The mask to use for the focused dimension when there is maximal focus.
         */
        private const val MaxFocusMask = 0x3FFFF // 256K (18 bits)

        /**
         * The number of bits to use for the non-focused dimension when there is maximal focus.
         */
        private const val MaxNonFocusBits = 13

        /**
         * The mask to use for the non-focused dimension when there is maximal focus.
         */
        private const val MaxNonFocusMask = 0x1FFF // 8K (13 bits)

        /**
         * Minimum Height shift offsets into Long value, indexed by FocusMask
         * Max offsets are these + 31
         * Width offsets are always either 2 (min) or 33 (max)
         */
        private val MinHeightOffsets = intArrayOf(
            18, // MinFocusWidth: 2 + 16
            20, // MaxFocusWidth: 2 + 18
            17, // MinFocusHeight: 2 + 15
            15 // MaxFocusHeight: 2 + 13
        )

        /**
         * The mask to use for both minimum and maximum width.
         */
        private val WidthMask = intArrayOf(
            MinFocusMask, // MinFocusWidth (16 bits)
            MaxFocusMask, // MaxFocusWidth (18 bits)
            MinNonFocusMask, // MinFocusHeight (15 bits)
            MaxNonFocusMask // MaxFocusHeight (13 bits)
        )

        /**
         * The mask to use for both minimum and maximum height.
         */
        private val HeightMask = intArrayOf(
            MinNonFocusMask, // MinFocusWidth (15 bits)
            MaxNonFocusMask, // MaxFocusWidth (13 bits)
            MinFocusMask, // MinFocusHeight (16 bits)
            MaxFocusMask // MaxFocusHeight (18 bits)
        )

        /**
         * Creates constraints for fixed size in both dimensions.
         */
        @Stable
        fun fixed(
            width: Int,
            height: Int
        ): Constraints {
            require(width >= 0 && height >= 0) {
                "width($width) and height($height) must be >= 0"
            }
            return createConstraints(width, width, height, height)
        }

        /**
         * Creates constraints for fixed width and unspecified height.
         */
        @Stable
        fun fixedWidth(
            width: Int
        ): Constraints {
            require(width >= 0) {
                "width($width) must be >= 0"
            }
            return createConstraints(
                minWidth = width,
                maxWidth = width,
                minHeight = 0,
                maxHeight = Infinity
            )
        }

        /**
         * Creates constraints for fixed height and unspecified width.
         */
        @Stable
        fun fixedHeight(
            height: Int
        ): Constraints {
            require(height >= 0) {
                "height($height) must be >= 0"
            }
            return createConstraints(
                minWidth = 0,
                maxWidth = Infinity,
                minHeight = height,
                maxHeight = height
            )
        }

        /**
         * Creates a [Constraints], only checking that the values fit in the packed Long.
         */
        internal fun createConstraints(
            minWidth: Int,
            maxWidth: Int,
            minHeight: Int,
            maxHeight: Int
        ): Constraints {
            val heightVal = if (maxHeight == Infinity) minHeight else maxHeight
            val heightBits = bitsNeedForSize(heightVal)

            val widthVal = if (maxWidth == Infinity) minWidth else maxWidth
            val widthBits = bitsNeedForSize(widthVal)

            if (widthBits + heightBits > 31) {
                throw IllegalArgumentException(
                    "Can't represent a width of $widthVal and height " +
                        "of $heightVal in Constraints"
                )
            }

            val focus = when (widthBits) {
                MinNonFocusBits -> MinFocusHeight
                MinFocusBits -> MinFocusWidth
                MaxNonFocusBits -> MaxFocusHeight
                MaxFocusBits -> MaxFocusWidth
                else -> throw IllegalStateException("Should only have the provided constants.")
            }

            val maxWidthValue = if (maxWidth == Infinity) 0 else maxWidth + 1
            val maxHeightValue = if (maxHeight == Infinity) 0 else maxHeight + 1

            val minHeightOffset = MinHeightOffsets[focus.toInt()]
            val maxHeightOffset = minHeightOffset + 31

            val value = focus or
                (minWidth.toLong() shl 2) or
                (maxWidthValue.toLong() shl 33) or
                (minHeight.toLong() shl minHeightOffset) or
                (maxHeightValue.toLong() shl maxHeightOffset)
            return Constraints(value)
        }

        private fun bitsNeedForSize(size: Int): Int {
            return when {
                size < MaxNonFocusMask -> MaxNonFocusBits
                size < MinNonFocusMask -> MinNonFocusBits
                size < MinFocusMask -> MinFocusBits
                size < MaxFocusMask -> MaxFocusBits
                else -> throw IllegalArgumentException(
                    "Can't represent a size of $size in " +
                        "Constraints"
                )
            }
        }
    }
}

/**
 * Create a [Constraints]. [minWidth] and [minHeight] must be positive and
 * [maxWidth] and [maxHeight] must be greater than or equal to [minWidth] and [minHeight],
 * respectively, or [Infinity][Constraints.Infinity].
 */
@Stable
fun Constraints(
    minWidth: Int = 0,
    maxWidth: Int = Constraints.Infinity,
    minHeight: Int = 0,
    maxHeight: Int = Constraints.Infinity
): Constraints {
    require(maxWidth >= minWidth) {
        "maxWidth($maxWidth) must be >= than minWidth($minWidth)"
    }
    require(maxHeight >= minHeight) {
        "maxHeight($maxHeight) must be >= than minHeight($minHeight)"
    }
    require(minWidth >= 0 && minHeight >= 0) {
        "minWidth($minWidth) and minHeight($minHeight) must be >= 0"
    }
    return Constraints.createConstraints(minWidth, maxWidth, minHeight, maxHeight)
}

/**
 * Takes [otherConstraints] and returns the result of coercing them in the current constraints.
 * Note this means that any size satisfying the resulting constraints will satisfy the current
 * constraints, but they might not satisfy the [otherConstraints] when the two set of constraints
 * are disjoint.
 * Examples (showing only width, height works the same):
 * (minWidth=2, maxWidth=10).constrain(minWidth=7, maxWidth=12) -> (minWidth = 7, maxWidth = 10)
 * (minWidth=2, maxWidth=10).constrain(minWidth=11, maxWidth=12) -> (minWidth=10, maxWidth=10)
 * (minWidth=2, maxWidth=10).constrain(minWidth=5, maxWidth=7) -> (minWidth=5, maxWidth=7)
 */
fun Constraints.constrain(otherConstraints: Constraints) = Constraints(
    minWidth = otherConstraints.minWidth.coerceIn(minWidth, maxWidth),
    maxWidth = otherConstraints.maxWidth.coerceIn(minWidth, maxWidth),
    minHeight = otherConstraints.minHeight.coerceIn(minHeight, maxHeight),
    maxHeight = otherConstraints.maxHeight.coerceIn(minHeight, maxHeight)
)

/**
 * Takes a size and returns the closest size to it that satisfies the constraints.
 */
@Stable
fun Constraints.constrain(size: IntSize) = IntSize(
    width = size.width.coerceIn(minWidth, maxWidth),
    height = size.height.coerceIn(minHeight, maxHeight)
)

/**
 * Takes a width and returns the closest size to it that satisfies the constraints.
 */
@Stable
fun Constraints.constrainWidth(width: Int) = width.coerceIn(minWidth, maxWidth)

/**
 * Takes a height and returns the closest size to it that satisfies the constraints.
 */
@Stable
fun Constraints.constrainHeight(height: Int) = height.coerceIn(minHeight, maxHeight)

/**
 * Takes a size and returns whether it satisfies the current constraints.
 */
@Stable
fun Constraints.isSatisfiedBy(size: IntSize): Boolean {
    return size.width in minWidth..maxWidth && size.height in minHeight..maxHeight
}

/**
 * Returns the Constraints obtained by offsetting the current instance with the given values.
 */
@Stable
fun Constraints.offset(horizontal: Int = 0, vertical: Int = 0) = Constraints(
    (minWidth + horizontal).coerceAtLeast(0),
    addMaxWithMinimum(maxWidth, horizontal),
    (minHeight + vertical).coerceAtLeast(0),
    addMaxWithMinimum(maxHeight, vertical)
)

private fun addMaxWithMinimum(max: Int, value: Int): Int {
    return if (max == Constraints.Infinity) {
        max
    } else {
        (max + value).coerceAtLeast(0)
    }
}