CurvedRow.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.wear.tiles.curved

import androidx.glance.GlanceNode
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.glance.Emittable
import androidx.glance.EmittableWithChildren
import androidx.glance.GlanceModifier
import androidx.glance.unit.ColorProvider

/**
 * A curved layout container. This container will fill itself to a circle, which fits inside its
 * parent container, and all of its children will be placed on that circle. The parameters
 * [anchorDegrees] and [anchorType] can be used to specify where to draw children within this
 * circle. Each child will then be placed, one after the other, clockwise around the circle.
 *
 * While this container can hold any composable element, only those built specifically to work
 * inside of a CurvedRow container (e.g. [CurvedScope.curvedText]) will adapt themselves to the
 * CurvedRow. Any other element wrapped in [CurvedScope.curvedComposable] will be drawn normally,
 * at a tangent to the circle or straight up depending on the value of rotateContent.
 *
 * @param modifier Modifiers for this container.
 * @param anchorDegrees The angle for the anchor in degrees, used with [anchorType] to determine
 *   where to draw children. Note that 0 degrees is the 3 o'clock position on a device, and the
 *   angle sweeps clockwise. Values do not have to be clamped to the range 0-360; values less
 *   than 0 degrees will sweep anti-clockwise (i.e. -90 degrees is equivalent to 270 degrees),
 *   and values >360 will be be placed at X mod 360 degrees.
 * @param anchorType Alignment of the contents of this container relative to [anchorDegrees].
 * @param radialAlignment specifies where to lay down children that are thinner than the
 *   CurvedRow, either closer to the center (INNER), apart from the center (OUTER) or in the middle
 *   point (CENTER).
 * @param content The content of this [CurvedRow].
 */
@Composable
public fun CurvedRow(
    modifier: GlanceModifier = GlanceModifier,
    anchorDegrees: Float = 270f,
    anchorType: AnchorType = AnchorType.Center,
    radialAlignment: RadialAlignment = RadialAlignment.Center,
    content: CurvedScope.() -> Unit
) {
    GlanceNode(
        factory = ::EmittableCurvedRow,
        update = {
            this.set(modifier) { this.modifier = it }
            this.set(anchorDegrees) { this.anchorDegrees = it }
            this.set(anchorType) { this.anchorType = it }
            this.set(radialAlignment) { this.radialAlignment = it }
        },
        content = applyCurvedScope(content)
    )
}

private fun applyCurvedScope(
    content: CurvedScope.() -> Unit
): @Composable () -> Unit {
    val curvedChildList = mutableListOf<@Composable CurvedChildScope.() -> Unit>()
    val curvedScopeImpl = object : CurvedScope {
        override fun curvedComposable(
            rotateContent: Boolean,
            content: @Composable () -> Unit
        ) {
            curvedChildList.add { CurvedChild(rotateContent, content) }
        }

        override fun curvedText(
            text: String,
            curvedModifier: GlanceCurvedModifier,
            style: CurvedTextStyle?
        ) {
            curvedChildList.add {
                GlanceNode(
                    factory = ::EmittableCurvedText,
                    update = {
                        this.set(text) { this.text = it }
                        this.set(curvedModifier) { this.curvedModifier = it }
                        this.set(style) { this.style = it }
                    }
                )
            }
        }

        override fun curvedLine(color: ColorProvider, curvedModifier: GlanceCurvedModifier) {
            curvedChildList.add {
                GlanceNode(
                    factory = ::EmittableCurvedLine,
                    update = {
                        this.set(color) { this.color = it }
                        this.set(curvedModifier) { this.curvedModifier = it }
                    }
                )
            }
        }

        override fun curvedSpacer(curvedModifier: GlanceCurvedModifier) {
            curvedChildList.add {
                GlanceNode(
                    factory = ::EmittableCurvedSpacer,
                    update = {
                        this.set(curvedModifier) { this.curvedModifier = it }
                    }
                )
            }
        }
    }

    curvedScopeImpl.apply(content)

    return {
        curvedChildList.forEach { composable ->
            object : CurvedChildScope {}.apply { composable() }
        }
    }
}

@Composable
private fun CurvedChild(
    rotateContent: Boolean,
    content: @Composable () -> Unit
) {
    GlanceNode(
        factory = ::EmittableCurvedChild,
        update = {
            this.set(rotateContent) { this.rotateContent = it }
        },
        content = content
    )
}

internal class EmittableCurvedRow : EmittableWithChildren() {
    override var modifier: GlanceModifier = GlanceModifier

    var anchorDegrees: Float = 270f
    var anchorType: AnchorType = AnchorType.Center
    var radialAlignment: RadialAlignment = RadialAlignment.Center

    override fun toString() =
        "EmittableCurvedRow(modifier=$modifier, anchorDegrees=$anchorDegrees," +
            "anchorType=$anchorType, children=[\n{${childrenToString()}}\n])"
}

internal class EmittableCurvedChild : EmittableWithChildren() {
    override var modifier: GlanceModifier = GlanceModifier

    var rotateContent: Boolean = false
}

internal class EmittableCurvedText : Emittable {
    override var modifier: GlanceModifier = GlanceModifier

    var curvedModifier: GlanceCurvedModifier = GlanceCurvedModifier
    var text: String = ""
    var style: CurvedTextStyle? = null
}

internal class EmittableCurvedLine : Emittable {
    override var modifier: GlanceModifier = GlanceModifier

    var color: ColorProvider = ColorProvider(Color.Transparent)
    var curvedModifier: GlanceCurvedModifier = GlanceCurvedModifier
}

internal class EmittableCurvedSpacer : Emittable {
    override var modifier: GlanceModifier = GlanceModifier

    var curvedModifier: GlanceCurvedModifier = GlanceCurvedModifier
}

@DslMarker
annotation class CurvedScopeMarker

@CurvedScopeMarker
interface CurvedChildScope

/** A scope for elements which can only be contained within a [CurvedRow]. */
@CurvedScopeMarker
interface CurvedScope {

    /**
     * Component that allows normal composable to be part of a [CurvedRow]
     *
     * @param rotateContent whether to rotate the composable at a tangent to the circle
     * @param content The content of this [curvedComposable].
     */
    fun curvedComposable(
        rotateContent: Boolean = true,
        content: @Composable () -> Unit
    )

    /**
     * A text element which will draw curved text. This is only valid as a direct descendant of a
     * [CurvedRow]
     *
     * Note: The sweepAngle/thickness from curvedModifier is ignored by CurvedText, its size is measured
     * with the set text and text style
     *
     * @param text The text to render.
     * @param curvedModifier [GlanceCurvedModifier] to apply to this layout element.
     * @param style The style to use for the Text.
     */
    // TODO(b/227327952) Make CurvedText accepts sweepAngle/thickness in CurveModifier
    fun curvedText(
        text: String,
        curvedModifier: GlanceCurvedModifier = GlanceCurvedModifier,
        style: CurvedTextStyle? = null
    )

    /**
     * A line that can be used in a [CurvedRow] and renders as a curved bar.
     *
     * @param color The color of this line.
     * @param curvedModifier [GlanceCurvedModifier] to apply to this layout element.
     */
    fun curvedLine(
        color: ColorProvider,
        curvedModifier: GlanceCurvedModifier = GlanceCurvedModifier
    )

    /**
     * A simple spacer used to provide padding between adjacent elements in a [CurvedRow].
     *
     * @param curvedModifier [GlanceCurvedModifier] to apply to this layout element.
     */
    fun curvedSpacer(
        curvedModifier: GlanceCurvedModifier = GlanceCurvedModifier
    )
}