CurvedPadding.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.wear.compose.foundation

import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp

/**
 * Apply additional space along the edges of the content.
 *
 * @param paddingValues The [ArcPaddingValues] to use. See that class and factory methods to see
 * how paddings can be specified.
 */
public fun CurvedModifier.padding(paddingValues: ArcPaddingValues) =
    this.then { child -> PaddingWrapper(child, paddingValues) }

/**
 * Apply additional space along the edges of the content. Dimmensions are in dp. For before and
 * after they will be considered as if they are at the midpoint of the content (for conversion
 * between dimension and angle).
 *
 * @param outer The space to add to the outer edge of the content (away from the center of the
 * containing CurvedLayout)
 * @param inner The space to add to the inner edge of the content (towards the center of the
 * containing CurvedLayout)
 * @param before The space added before the component, if it was draw clockwise. This is the edge of
 * the component with the "smallest" angle.
 * @param after The space added after the component, if it was draw clockwise. This is the edge of
 * the component with the "biggest" angle.
 */
public fun CurvedModifier.padding(outer: Dp, inner: Dp, before: Dp, after: Dp) =
    padding(ArcPaddingValuesImpl(outer, inner, before, after))

/**
 * Apply [angular] dp space before and after the component, and [radial] dp space to the outer and
 * inner edges.
 *
 * @param radial The space added to the outer and inner edges of the content, in dp.
 * @param angular The space added before and after the content, in dp.
 */
public fun CurvedModifier.padding(radial: Dp = 0.dp, angular: Dp = 0.dp) =
    padding(radial, radial, angular, angular)

/**
 * Apply [all] dp space around the component.
 *
 * @param all The space added to all edges.
 */
public fun CurvedModifier.padding(all: Dp = 0.dp) = padding(all, all, all, all)

/**
 * Apply additional space along each edge of the content in [Dp].
 * See the [ArcPaddingValues] factories for convenient ways to
 * build [ArcPaddingValues].
 */
@Stable
public interface ArcPaddingValues {
    /**
     * Padding in the outward direction from the center of the [CurvedLayout]
     */
    fun calculateOuterPadding(radialDirection: CurvedDirection.Radial): Dp

    /**
     * Padding in the inwards direction towards the center of the [CurvedLayout]
     */
    fun calculateInnerPadding(radialDirection: CurvedDirection.Radial): Dp

    /**
     * Padding added before the component, if it was draw clockwise. This is the edge of the
     * component with the "smallest" angle.
     */
    fun calculateAfterPadding(
        layoutDirection: LayoutDirection,
        angularDirection: CurvedDirection.Angular
    ): Dp

    /**
     * Padding added after the component, if it was draw clockwise. This is the edge of the
     * component with the "biggest" angle.
     */
    fun calculateBeforePadding(
        layoutDirection: LayoutDirection,
        angularDirection: CurvedDirection.Angular
    ): Dp
}

/**
 * Apply additional space along each edge of the content in [Dp]. Note that that all dimensions are
 * applied to a concrete edge, indepenend on layout direction and curved layout direction.
 *
 * @param outer Padding in the outward direction from the center of the
 * [CurvedLayout]
 * @param inner Padding in the inwards direction towards the center of the [CurvedLayout]
 * @param before Padding added before the component, if it was draw clockwise.
 * @param after Padding added after the component, if it was draw clockwise.
 */
public fun ArcPaddingValues(
    outer: Dp = 0.dp,
    inner: Dp = 0.dp,
    before: Dp = 0.dp,
    after: Dp = 0.dp
): ArcPaddingValues =
    ArcPaddingValuesImpl(outer, inner, before, after)

/**
 * Apply [all] dp of additional space along each edge of the content.
 */
public fun ArcPaddingValues(all: Dp): ArcPaddingValues = ArcPaddingValuesImpl(all, all, all, all)

/**
 * Apply [radial] dp of additional space on the edges towards and away from the center, and
 * [angular] dp before and after the component.
 */
public fun ArcPaddingValues(radial: Dp = 0.dp, angular: Dp = 0.dp): ArcPaddingValues =
    ArcPaddingValuesImpl(radial, radial, angular, angular)

@Immutable
internal class ArcPaddingValuesImpl(val outer: Dp, val inner: Dp, val before: Dp, val after: Dp) :
    ArcPaddingValues {
    override fun equals(other: Any?): Boolean {
        return other is ArcPaddingValuesImpl &&
            outer == other.outer &&
            inner == other.inner &&
            before == other.before &&
            after == other.after
    }

    override fun hashCode() = ((outer.hashCode() * 31 + inner.hashCode()) * 31 +
        before.hashCode()) * 31 + after.hashCode()

    override fun toString(): String {
        return "ArcPaddingValuesImpl(outer=$outer, inner=$inner, before=$before, after=$after)"
    }

    override fun calculateOuterPadding(radialDirection: CurvedDirection.Radial) = outer
    override fun calculateInnerPadding(radialDirection: CurvedDirection.Radial) = inner
    override fun calculateBeforePadding(
        layoutDirection: LayoutDirection,
        angularDirection: CurvedDirection.Angular
    ) = before
    override fun calculateAfterPadding(
        layoutDirection: LayoutDirection,
        angularDirection: CurvedDirection.Angular
    ) = after
}

internal class PaddingWrapper(
    child: CurvedChild,
    val paddingValues: ArcPaddingValues
) : BaseCurvedChildWrapper(child) {
    private var outerPx = 0f
    private var innerPx = 0f
    private var beforePx = 0f
    private var afterPx = 0f

    override fun CurvedMeasureScope.initializeMeasure(
        measurables: Iterator<Measurable>
    ) {
        outerPx = paddingValues.calculateOuterPadding(curvedLayoutDirection.radial).toPx()
        innerPx = paddingValues.calculateInnerPadding(curvedLayoutDirection.radial).toPx()
        beforePx = paddingValues.calculateBeforePadding(
            curvedLayoutDirection.layoutDirection,
            curvedLayoutDirection.angular
        ).toPx()
        afterPx = paddingValues.calculateAfterPadding(
            curvedLayoutDirection.layoutDirection,
            curvedLayoutDirection.angular
        ).toPx()
        with(wrapped) {
            initializeMeasure(measurables)
        }
    }

    override fun doEstimateThickness(maxRadius: Float) = wrapped.estimateThickness(maxRadius) +
        outerPx + innerPx

    override fun doRadialPosition(
        parentOuterRadius: Float,
        parentThickness: Float
    ): PartialLayoutInfo {
        val partialLayoutInfo = wrapped.radialPosition(
            parentOuterRadius - outerPx,
            parentThickness - outerPx - innerPx
        )
        val angularPadding = (beforePx + afterPx) / partialLayoutInfo.measureRadius
        return PartialLayoutInfo(
            partialLayoutInfo.sweepRadians + angularPadding,
            partialLayoutInfo.outerRadius + outerPx,
            partialLayoutInfo.thickness + innerPx + outerPx,
            partialLayoutInfo.measureRadius
        )
    }

    override fun doAngularPosition(
        parentStartAngleRadians: Float,
        parentSweepRadians: Float,
        centerOffset: Offset
    ): Float {
        val startAngularPadding = beforePx / measureRadius
        val angularPadding = (beforePx + afterPx) / measureRadius
        return wrapped.angularPosition(
            parentStartAngleRadians + startAngularPadding,
            parentSweepRadians - angularPadding,
            centerOffset
        ) - startAngularPadding
    }
}