Box.kt

/*
 * Copyright 2020 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

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.emptyContent
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp

/**
 * A convenience composable that combines common layout and draw logic.
 *
 * In order to define the size of the [Box], the [androidx.compose.foundation.layout.width],
 * [androidx.compose.foundation.layout.height] and [androidx.compose.foundation.layout.size]
 * modifiers can be used.
 * The [Box] will try to be only as small as its content. However, if it is constrained
 * otherwise, [Box] will allow its content to be smaller and will position the content inside,
 * according to [gravity].
 *
 * The specified [padding] will be applied inside the [Box]. In order to apply padding outside
 * the [Box], the [androidx.compose.foundation.layout.padding] modifier should be used.
 *
 * @sample androidx.compose.foundation.samples.SimpleCircleBox
 *
 * @param modifier The modifier to be applied to the Box
 * @param shape The shape of the box
 * @param backgroundColor The [Color] for background with. If [Color.Transparent], there will be no
 * background
 * @param border [BorderStroke] object that specifies border appearance, such as size and color. If
 * `null`, there will be no border
 * @param padding The padding to be applied inside Box, along its edges. Unless otherwise
 * specified, content will be padded by the [BorderStroke.width], if [border] is provided
 * @param paddingStart sets the padding of the start edge. Setting this will override [padding]
 * for the start edge
 * @param paddingTop sets the padding of the top edge. Setting this will override [padding] for
 * the top edge
 * @param paddingEnd sets the padding of the end edge. Setting this will override [padding] for
 * the end edge
 * @param paddingBottom sets the padding of the bottom edge. Setting this will override [padding]
 * for the bottom edge
 * @param gravity The gravity of the content inside Box
 */
@Composable
fun Box(
    modifier: Modifier = Modifier,
    shape: Shape = RectangleShape,
    backgroundColor: Color = Color.Transparent,
    border: BorderStroke? = null,
    padding: Dp = border?.width ?: 0.dp,
    paddingStart: Dp = Dp.Unspecified,
    paddingTop: Dp = Dp.Unspecified,
    paddingEnd: Dp = Dp.Unspecified,
    paddingBottom: Dp = Dp.Unspecified,
    gravity: ContentGravity = ContentGravity.TopStart,
    children: @Composable () -> Unit = emptyContent()
) {
    val borderModifier =
        if (border != null) Modifier.border(border, shape) else Modifier
    val backgroundModifier =
        if (backgroundColor != Color.Transparent) {
            Modifier.background(color = backgroundColor, shape = shape)
        } else {
            Modifier
        }
    val paddingModifier =
        if (needsPadding(padding, paddingStart, paddingTop, paddingEnd, paddingBottom)) {
            Modifier.padding(
                if (paddingStart != Dp.Unspecified) paddingStart else padding,
                if (paddingTop != Dp.Unspecified) paddingTop else padding,
                if (paddingEnd != Dp.Unspecified) paddingEnd else padding,
                if (paddingBottom != Dp.Unspecified) paddingBottom else padding
            )
        } else {
            Modifier
        }
    // TODO(malkov): support ContentColor prorogation (b/148129218)

    val columnArrangement = gravity.toColumnArrangement()
    val columnGravity = gravity.toColumnGravity()
    Column(
        modifier = modifier.then(backgroundModifier).then(borderModifier).then(paddingModifier),
        verticalArrangement = columnArrangement,
        horizontalGravity = columnGravity
    ) {
        children()
    }
}

// TODO(popam/148014745): add a Gravity class consistent with cross axis alignment for Row/Column
typealias ContentGravity = Alignment

private fun needsPadding(
    padding: Dp,
    paddingStart: Dp,
    paddingTop: Dp,
    paddingEnd: Dp,
    paddingBottom: Dp
) = (padding != Dp.Unspecified && padding != 0.dp) ||
        (paddingStart != Dp.Unspecified && paddingStart != 0.dp) ||
        (paddingTop != Dp.Unspecified && paddingTop != 0.dp) ||
        (paddingEnd != Dp.Unspecified && paddingEnd != 0.dp) ||
        (paddingBottom != Dp.Unspecified && paddingBottom != 0.dp)

private fun Alignment.toColumnArrangement() = Arrangement.aligned(object : Alignment.Vertical {
    override fun align(size: Int): Int = align(IntSize(0, size)).y
})

private fun Alignment.toColumnGravity(): Alignment.Horizontal = object : Alignment.Horizontal {
    override fun align(size: Int, layoutDirection: LayoutDirection): Int {
        return align(IntSize(size, 0), layoutDirection).x
    }
}