Padding.kt

/*
 * Copyright 2021 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.glance.layout

import android.content.res.Resources
import androidx.annotation.DimenRes
import androidx.annotation.RestrictTo
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.glance.GlanceModifier

/**
 * Apply additional space along each edge of the content in [Dp]: [start], [top], [end] and
 * [bottom]. The start and end edges will be determined by layout direction of the current locale.
 * Padding is applied before content measurement and takes precedence; content may only be as large
 * as the remaining space.
 *
 * If any value is not defined, it will be [0.dp] or whatever value was defined by an earlier
 * modifier.
 */
public fun GlanceModifier.padding(
    start: Dp = 0.dp,
    top: Dp = 0.dp,
    end: Dp = 0.dp,
    bottom: Dp = 0.dp,
): GlanceModifier = this.then(
    PaddingModifier(
        start = start.toPadding(),
        top = top.toPadding(),
        end = end.toPadding(),
        bottom = bottom.toPadding(),
    )
)

/**
 * Apply additional space along each edge of the content in [Dp]: [start], [top], [end] and
 * [bottom]. The start and end edges will be determined by layout direction of the current locale.
 * Padding is applied before content measurement and takes precedence; content may only be as large
 * as the remaining space.
 *
 * If any value is not defined, it will be [0.dp] or whatever value was defined by an earlier
 * modifier.
 */
public fun GlanceModifier.padding(
    @DimenRes start: Int = 0,
    @DimenRes top: Int = 0,
    @DimenRes end: Int = 0,
    @DimenRes bottom: Int = 0
): GlanceModifier = this.then(
    PaddingModifier(
        start = start.toPadding(),
        top = top.toPadding(),
        end = end.toPadding(),
        bottom = bottom.toPadding(),
    )
)

/**
 * Apply [horizontal] dp space along the left and right edges of the content, and [vertical] dp
 * space along the top and bottom edges.
 *
 * If any value is not defined, it will be [0.dp] or whatever value was defined by an earlier
 * modifier.
 */
public fun GlanceModifier.padding(
    horizontal: Dp = 0.dp,
    vertical: Dp = 0.dp,
): GlanceModifier = this.then(
    PaddingModifier(
        start = horizontal.toPadding(),
        top = vertical.toPadding(),
        end = horizontal.toPadding(),
        bottom = vertical.toPadding(),
    )
)

/**
 * Apply [horizontal] dp space along the left and right edges of the content, and [vertical] dp
 * space along the top and bottom edges.
 *
 * If any value is not defined, it will be [0.dp] or whatever value was defined by an earlier
 * modifier.
 */
public fun GlanceModifier.padding(
    @DimenRes horizontal: Int = 0,
    @DimenRes vertical: Int = 0
): GlanceModifier = this.then(
    PaddingModifier(
        start = horizontal.toPadding(),
        top = vertical.toPadding(),
        end = horizontal.toPadding(),
        bottom = vertical.toPadding(),
    )
)

/**
 * Apply [all] dp of additional space along each edge of the content, left, top, right and bottom.
 */
public fun GlanceModifier.padding(all: Dp): GlanceModifier {
    val allDp = all.toPadding()
    return this.then(
        PaddingModifier(
            start = allDp,
            top = allDp,
            end = allDp,
            bottom = allDp,
        )
    )
}

/**
 * Apply [all] dp of additional space along each edge of the content, left, top, right and bottom.
 */
public fun GlanceModifier.padding(@DimenRes all: Int): GlanceModifier {
    val allDp = all.toPadding()
    return this.then(
        PaddingModifier(
            start = allDp,
            top = allDp,
            end = allDp,
            bottom = allDp,
        )
    )
}

/**
 *  Apply additional space along each edge of the content in [Dp]: [left], [top], [right] and
 * [bottom], ignoring the current locale's layout direction.
 */
public fun GlanceModifier.absolutePadding(
    left: Dp = 0.dp,
    top: Dp = 0.dp,
    right: Dp = 0.dp,
    bottom: Dp = 0.dp,
): GlanceModifier = this.then(
    PaddingModifier(
        left = left.toPadding(),
        top = top.toPadding(),
        right = right.toPadding(),
        bottom = bottom.toPadding(),
    )
)

/**
 *  Apply additional space along each edge of the content in [Dp]: [left], [top], [right] and
 * [bottom], ignoring the current locale's layout direction.
 */
public fun GlanceModifier.absolutePadding(
    @DimenRes left: Int = 0,
    @DimenRes top: Int = 0,
    @DimenRes right: Int = 0,
    @DimenRes bottom: Int = 0
): GlanceModifier = this.then(
    PaddingModifier(
        left = left.toPadding(),
        top = top.toPadding(),
        right = right.toPadding(),
        bottom = bottom.toPadding(),
    )
)

private fun Dp.toPadding() =
    PaddingDimension(dp = this)

private fun Int.toPadding() =
    if (this == 0) PaddingDimension() else PaddingDimension(this)

/** @suppress */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public fun GlanceModifier.collectPadding(): PaddingModifier? =
    foldIn<PaddingModifier?>(null) { acc, modifier ->
        if (modifier is PaddingModifier) {
            (acc ?: PaddingModifier()) + modifier
        } else {
            acc
        }
    }

/** @suppress */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public fun GlanceModifier.collectPaddingInDp(resources: Resources) =
    collectPadding()?.toDp(resources)

private fun List<Int>.toDp(resources: Resources) =
    fold(0.dp) { acc, res ->
        acc + (resources.getDimension(res) / resources.displayMetrics.density).dp
    }

/** @suppress */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public data class PaddingModifier(
    public val left: PaddingDimension = PaddingDimension(),
    public val start: PaddingDimension = PaddingDimension(),
    public val top: PaddingDimension = PaddingDimension(),
    public val right: PaddingDimension = PaddingDimension(),
    public val end: PaddingDimension = PaddingDimension(),
    public val bottom: PaddingDimension = PaddingDimension(),
) : GlanceModifier.Element {

    public operator fun plus(other: PaddingModifier) =
        PaddingModifier(
            left = left + other.left,
            start = start + other.start,
            top = top + other.top,
            right = right + other.right,
            end = end + other.end,
            bottom = bottom + other.bottom,
        )

    public fun toDp(resources: Resources): PaddingInDp =
        PaddingInDp(
            left = left.dp + left.resourceIds.toDp(resources),
            start = start.dp + start.resourceIds.toDp(resources),
            top = top.dp + top.resourceIds.toDp(resources),
            right = right.dp + right.resourceIds.toDp(resources),
            end = end.dp + end.resourceIds.toDp(resources),
            bottom = bottom.dp + bottom.resourceIds.toDp(resources),
        )
}

/** @suppress */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public data class PaddingDimension(
    public val dp: Dp = 0.dp,
    public val resourceIds: List<Int> = emptyList(),
) {
    constructor(@DimenRes resource: Int) : this(resourceIds = listOf(resource))

    public operator fun plus(other: PaddingDimension) =
        PaddingDimension(
            dp = dp + other.dp,
            resourceIds = resourceIds + other.resourceIds,
        )

    companion object {
        val Zero = PaddingDimension()
    }
}

/** @suppress */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public data class PaddingInDp(
    public val left: Dp = 0.dp,
    public val start: Dp = 0.dp,
    public val top: Dp = 0.dp,
    public val right: Dp = 0.dp,
    public val end: Dp = 0.dp,
    public val bottom: Dp = 0.dp,
) {
    /** Transfer [start] / [end] to [left] / [right] depending on [isRtl]. */
    public fun toAbsolute(isRtl: Boolean) =
        PaddingInDp(
            left = left + if (isRtl) end else start,
            top = top,
            right = right + if (isRtl) start else end,
            bottom = bottom,
        )

    /** Transfer [left] / [right] to [start] / [end] depending on [isRtl]. */
    public fun toRelative(isRtl: Boolean) =
        PaddingInDp(
            start = start + if (isRtl) right else left,
            top = top,
            end = end + if (isRtl) left else right,
            bottom = bottom
        )
}