CurvedDraw.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.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import kotlin.math.PI

/**
 * Specified a solid background for a curved element.
 *
 * @param color The color to use to paint the background.
 * @param cap How to start and end the background.
 */
public fun CurvedModifier.background(
    color: Color,
    cap: StrokeCap = StrokeCap.Butt,
) = background(cap) { SolidColor(color) }

/**
 * Specifies a radial gradient background for a curved element.
 *
 * Example usage:
 * @sample androidx.wear.compose.foundation.samples.CurvedBackground
 *
 * @param colorStops Colors and their offset in the gradient area.
 * Note that the offsets should be in ascending order. 0 means the outer curve and
 * 1 means the inner curve of the curved element.
 * @param cap How to start and end the background.
 */
public fun CurvedModifier.radialGradientBackground(
    vararg colorStops: Pair<Float, Color>,
    cap: StrokeCap = StrokeCap.Butt
) = background(cap) { layoutInfo ->
    val radiusRatio = layoutInfo.innerRadius / layoutInfo.outerRadius
    Brush.radialGradient(
        *(colorStops.map { (step, color) ->
            1f - step * (1f - radiusRatio) to color
        }.reversed().toTypedArray()),
        center = layoutInfo.centerOffset,
        radius = layoutInfo.outerRadius
    )
}

/**
 * Specifies a radial gradient background for a curved element.
 *
 * @param colors Colors in the gradient area. Gradient goes from the outer curve to the
 * inner curve of the curved element.
 * @param cap How to start and end the background.
 */
public fun CurvedModifier.radialGradientBackground(
    colors: List<Color>,
    cap: StrokeCap = StrokeCap.Butt
) = radialGradientBackground(*colorsToColorStops(colors), cap = cap)

/**
 * Specifies a sweep gradient background for a curved element.
 *
 * Example usage:
 * @sample androidx.wear.compose.foundation.samples.CurvedBackground
 *
 * @param colorStops Colors and their offset in the gradient area.
 * Note that the offsets should be in ascending order. 0 means where the curved element starts
 * laying out, 1 means the end
 * @param cap How to start and end the background.
 */
public fun CurvedModifier.angularGradientBackground(
    vararg colorStops: Pair<Float, Color>,
    cap: StrokeCap = StrokeCap.Butt
) = background(cap) { layoutInfo ->
    val actualStops = colorStops.map { (step, color) ->
        (layoutInfo.startAngleRadians + layoutInfo.sweepRadians * step) /
            (2 * PI).toFloat() to color
    }.sortedBy { it.first }
    Brush.sweepGradient(*(actualStops.toTypedArray()))
}

/**
 * Specifies a sweep gradient background for a curved element.
 *
 * @param colors Colors in the gradient area. Gradient goes in the clockwise direction.
 * @param cap How to start and end the background.
 */
public fun CurvedModifier.angularGradientBackground(
    colors: List<Color>,
    cap: StrokeCap = StrokeCap.Butt
) = angularGradientBackground(*colorsToColorStops(colors), cap = cap)

private fun colorsToColorStops(colors: List<Color>): Array<Pair<Float, Color>> =
    Array(colors.size) {
        it.toFloat() / (colors.size - 1) to colors[it]
    }

internal fun CurvedModifier.background(
    cap: StrokeCap = StrokeCap.Butt,
    brushProvider: (CurvedLayoutInfo) -> Brush
) = drawBefore {
    with(it) {
        val radius = outerRadius - thickness / 2
        drawArc(
            brushProvider(it),
            startAngleRadians.toDegrees(),
            sweepRadians.toDegrees(),
            useCenter = false,
            topLeft = centerOffset - Offset(radius, radius),
            size = Size(2 * radius, 2 * radius),
            style = Stroke(thickness, cap = cap)
        )
    }
}

internal class DrawWrapper(
    child: CurvedChild,
    val customDraw: DrawScope.(CurvedLayoutInfo) -> Unit,
    val drawBefore: Boolean
) : BaseCurvedChildWrapper(child) {

    private var parentOuterRadius: Float = 0f
    private var parentThickness: Float = 0f

    override fun doRadialPosition(
        parentOuterRadius: Float,
        parentThickness: Float,
    ): PartialLayoutInfo {
        this.parentThickness = parentThickness
        this.parentOuterRadius = parentOuterRadius
        return wrapped.radialPosition(
            parentOuterRadius,
            parentThickness,
        )
    }

    private lateinit var outerLayoutInfo: CurvedLayoutInfo

    override fun doAngularPosition(
        parentStartAngleRadians: Float,
        parentSweepRadians: Float,
        centerOffset: Offset
    ): Float {
        /* We want the background to fill the space that our parent assigned us (outerLayoutInfo),
         * as opposed to the size of or wrapped child (layoutInfo).
         */
        outerLayoutInfo = CurvedLayoutInfo(
            sweepRadians = parentSweepRadians,
            outerRadius = parentOuterRadius,
            thickness = parentThickness,
            centerOffset = centerOffset,
            measureRadius = parentOuterRadius - parentThickness / 2f,
            startAngleRadians = parentStartAngleRadians
        )
        return wrapped.angularPosition(
            parentStartAngleRadians,
            parentSweepRadians,
            centerOffset
        )
    }

    override fun DrawScope.draw() {
        if (drawBefore) customDraw(outerLayoutInfo)
        with(wrapped) {
            draw()
        }
        if (!drawBefore) customDraw(outerLayoutInfo)
    }
}

internal fun CurvedModifier.drawAfter(draw: DrawScope.(CurvedLayoutInfo) -> Unit) =
    this.then { child -> DrawWrapper(child, draw, drawBefore = false) }

internal fun CurvedModifier.drawBefore(draw: DrawScope.(CurvedLayoutInfo) -> Unit) =
    this.then { child -> DrawWrapper(child, draw, drawBefore = true) }