CurvedColumn.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.foundation.layout.Column
import androidx.compose.ui.geometry.Offset

/**
 * A curved layout composable that places its children stacked as part of an arc (the first child
 * will be the outermost). This is similar to a [Column] layout, that it's curved into a segment of
 * an annulus.
 *
 * The thickness of the layout (the difference between the outer and inner radius) will be the
 * sum of the thickness of its children, and the angle taken will be the biggest angle of the
 * children.
 *
 * Example usage:
 * @sample androidx.wear.compose.foundation.samples.CurvedRowAndColumn
 *
 * @param modifier The [CurvedModifier] to apply to this curved column.
 * @param radialDirection Order to lay out components, outside in or inside out. The default is to
 * inherit from the containing [curvedColumn] or [CurvedLayout]
 * @param angularAlignment Angular alignment specifies where to lay down children that are thinner
 * than the curved column, either at the (START) of the layout, at the (END), or (CENTER).
 * If unspecified or null, they can choose for themselves.
 */
public fun CurvedScope.curvedColumn(
    modifier: CurvedModifier = CurvedModifier,
    radialDirection: CurvedDirection.Radial? = null,
    angularAlignment: CurvedAlignment.Angular? = null,
    contentBuilder: CurvedScope.() -> Unit
) = add(
    CurvedColumnChild(
        curvedLayoutDirection.copy(overrideRadial = radialDirection),
        angularAlignment,
        contentBuilder
    ),
    modifier
)

internal class CurvedColumnChild(
    curvedLayoutDirection: CurvedLayoutDirection,
    private val angularAlignment: CurvedAlignment.Angular?,
    contentBuilder: CurvedScope.() -> Unit
) : ContainerChild(curvedLayoutDirection, !curvedLayoutDirection.outsideIn(), contentBuilder) {
    override fun doEstimateThickness(maxRadius: Float): Float =
        maxRadius - children.fold(maxRadius) { currentMaxRadius, node ->
            currentMaxRadius - node.estimateThickness(currentMaxRadius)
        }

    override fun doRadialPosition(
        parentOuterRadius: Float,
        parentThickness: Float,
    ): PartialLayoutInfo {
        // Compute space used by weighted children and space left
        val weights = childrenInLayoutOrder.map { node ->
            (node.computeParentData() as? CurvedScopeParentData)?.weight ?: 0f
        }
        val sumWeights = weights.sum()
        val extraSpace = parentThickness - childrenInLayoutOrder.mapIndexed { ix, node ->
            if (weights[ix] == 0f) {
                node.estimatedThickness
            } else {
                0f
            }
        }.sum()

        // position children
        var outerRadius = parentOuterRadius
        childrenInLayoutOrder.forEachIndexed { ix, node ->
            val actualThickness = if (weights[ix] > 0f) {
                    extraSpace * weights[ix] / sumWeights
                } else {
                    node.estimatedThickness
                }

            node.radialPosition(
                outerRadius,
                actualThickness
            )
            outerRadius -= actualThickness
        }
        var maxSweep = childrenInLayoutOrder.maxOfOrNull { it.sweepRadians } ?: 0f

        return PartialLayoutInfo(
            maxSweep,
            parentOuterRadius,
            parentOuterRadius - outerRadius,
            (parentOuterRadius + outerRadius) / 2 // ?
        )
    }

    override fun doAngularPosition(
        parentStartAngleRadians: Float,
        parentSweepRadians: Float,
        centerOffset: Offset
    ): Float {
        children.forEach { child ->
            var childAngularPosition = parentStartAngleRadians
            var childSweep = parentSweepRadians
            if (angularAlignment != null) {
                childAngularPosition = parentStartAngleRadians + angularAlignment.ratio *
                    (parentSweepRadians - child.sweepRadians)
                childSweep = child.sweepRadians
            }

            child.angularPosition(
                childAngularPosition,
                childSweep,
                centerOffset
            )
        }
        return parentStartAngleRadians
    }
}