WindowInsetsSize.kt

/*
 * Copyright 2022 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.
 */

package androidx.compose.foundation.layout

import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.LayoutModifier
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.InspectorValueInfo
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection

/**
 * Sets the width to that of [insets] at the [start][androidx.compose.ui.Alignment.Start]
 * of the screen, using either [left][WindowInsets.getLeft] or [right][WindowInsets.getRight],
 * depending on the [LayoutDirection].
 *
 * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
 *
 * @sample androidx.compose.foundation.layout.samples.insetsStartWidthSample
 */
@Suppress("ModifierInspectorInfo")
@Stable
fun Modifier.windowInsetsStartWidth(insets: WindowInsets) = this.then(
    DerivedWidthModifier(insets, debugInspectorInfo {
        name = "insetsStartWidth"
        properties["insets"] = insets
    }) { layoutDirection, density ->
        if (layoutDirection == LayoutDirection.Ltr) {
            getLeft(density, layoutDirection)
        } else {
            getRight(density, layoutDirection)
        }
    }
)

/**
 * Sets the width to that of [insets] at the [end][androidx.compose.ui.Alignment.End]
 * of the screen, using either [left][WindowInsets.getLeft] or [right][WindowInsets.getRight],
 * depending on the [LayoutDirection].
 *
 * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
 *
 * @sample androidx.compose.foundation.layout.samples.insetsEndWidthSample
 */
@Suppress("ModifierInspectorInfo")
@Stable
fun Modifier.windowInsetsEndWidth(insets: WindowInsets) = this.then(
    DerivedWidthModifier(insets, debugInspectorInfo {
        name = "insetsEndWidth"
        properties["insets"] = insets
    }) { layoutDirection, density ->
        if (layoutDirection == LayoutDirection.Rtl) {
            getLeft(density, layoutDirection)
        } else {
            getRight(density, layoutDirection)
        }
    }
)

/**
 * Sets the height to that of [insets] at the [top][WindowInsets.getTop] of the screen.
 *
 * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
 *
 * @sample androidx.compose.foundation.layout.samples.insetsTopHeightSample
 */
@Suppress("ModifierInspectorInfo")
@Stable
fun Modifier.windowInsetsTopHeight(insets: WindowInsets) = this.then(
    DerivedHeightModifier(insets, debugInspectorInfo {
        name = "insetsTopHeight"
        properties["insets"] = insets
    }) {
        getTop(it)
    }
)

/**
 * Sets the height to that of [insets] at the [bottom][WindowInsets.getBottom] of the screen.
 *
 * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
 *
 * @sample androidx.compose.foundation.layout.samples.insetsBottomHeightSample
 */
@Suppress("ModifierInspectorInfo")
@Stable
fun Modifier.windowInsetsBottomHeight(insets: WindowInsets) = this.then(
    DerivedHeightModifier(insets, debugInspectorInfo {
        name = "insetsBottomHeight"
        properties["insets"] = insets
    }) {
        getBottom(it)
    }
)

/**
 * Sets the width based on [widthCalc]. If the width is 0, the height will also always be 0
 * and the content will not be placed.
 */
@Stable
private class DerivedWidthModifier(
    private val insets: WindowInsets,
    inspectorInfo: InspectorInfo.() -> Unit,
    private val widthCalc: WindowInsets.(LayoutDirection, Density) -> Int
) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        val width = insets.widthCalc(layoutDirection, this)
        if (width == 0) {
            return layout(0, 0) { }
        }
        // check for height first
        val childConstraints = constraints.copy(minWidth = width, maxWidth = width)
        val placeable = measurable.measure(childConstraints)
        return layout(width, placeable.height) {
            placeable.placeRelative(0, 0)
        }
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) {
            return true
        }
        if (other !is DerivedWidthModifier) {
            return false
        }
        return insets == other.insets && widthCalc == other.widthCalc
    }

    override fun hashCode(): Int = 31 * insets.hashCode() + widthCalc.hashCode()
}

/**
 * Sets the height based on [heightCalc]. If the height is 0, the width will also always be 0
 * and the content will not be placed.
 */
@Stable
private class DerivedHeightModifier(
    private val insets: WindowInsets,
    inspectorInfo: InspectorInfo.() -> Unit,
    private val heightCalc: WindowInsets.(Density) -> Int
) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        val height = insets.heightCalc(this)
        if (height == 0) {
            return layout(0, 0) { }
        }
        // check for height first
        val childConstraints = constraints.copy(minHeight = height, maxHeight = height)
        val placeable = measurable.measure(childConstraints)
        return layout(placeable.width, height) {
            placeable.placeRelative(0, 0)
        }
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) {
            return true
        }
        if (other !is DerivedHeightModifier) {
            return false
        }
        return insets == other.insets && heightCalc == other.heightCalc
    }

    override fun hashCode(): Int = 31 * insets.hashCode() + heightCalc.hashCode()
}