API source code for Glimmer Icon component

When creating a Glimmer Icon component, refer to the following source code in Icon.kt:

/*
 * Copyright 2025 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.xr.glimmer

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.structuralEqualityPolicy
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.CacheDrawModifierNode
import androidx.compose.ui.draw.paint
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorProducer
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.toolingGraphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.node.DelegatingNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.invalidateDraw
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp

/**
 * An icon component that draws [imageVector], with a default size of [LocalIconSize], and applies a
 * content color tint. Icon is an opinionated component designed to be used with single-color icons
 * so that they can be tinted correctly for the component they are placed in. The recommended icon
 * sizes can be retrieved using [GlimmerTheme.Companion.iconSizes]. A size can be set explicitly
 * using [size], and components can use [LocalIconSize] to set the preferred size for any Icons
 * inside the component. The content color used to tint this icon is provided by [surface]. To set a
 * custom tint color, use the Icon overload with a tint parameter. For multicolored icons and icons
 * that should not be tinted, use the overload and set [Color.Unspecified] as the tint color. For
 * generic images that should not be tinted, and should not use the provided icon size, use the
 * generic [androidx.compose.foundation.Image] instead.
 *
 * @param imageVector [ImageVector] to draw inside this icon
 * @param contentDescription text used by accessibility services to describe what this icon
 *   represents. This should always be provided unless this icon is used for decorative purposes,
 *   and does not represent a meaningful action that a user can take. This text should be localized,
 *   such as by using [androidx.compose.ui.res.stringResource] or similar
 * @param modifier the [Modifier] to be applied to this icon
 * @see LocalIconSize
 * @see GlimmerTheme.Companion.iconSizes
 */
@Composable
public fun Icon(
    imageVector: ImageVector,
    contentDescription: String?,
    modifier: Modifier = Modifier,
) {
    Icon(
        painter = rememberVectorPainter(imageVector),
        contentDescription = contentDescription,
        modifier = modifier,
    )
}

/**
 * An icon component that draws [imageVector], with a default size of [LocalIconSize], and applies a
 * tint of [tint]. Icon is an opinionated component designed to be used with single-color icons so
 * that they can be tinted correctly for the component they are placed in. The recommended icon
 * sizes can be retrieved using [GlimmerTheme.Companion.iconSizes]. A size can be set explicitly
 * using [size], and components can use [LocalIconSize] to set the preferred size for any Icons
 * inside the component. Use the other overload of Icon without a [tint] parameter to apply the
 * recommended content color provided by a [surface]. For multicolored icons and icons that should
 * not be tinted, set [tint] to [Color.Unspecified]. For generic images that should not be tinted,
 * and should not use the provided icon size, use the generic [androidx.compose.foundation.Image]
 * instead.
 *
 * @param imageVector [ImageVector] to draw inside this icon
 * @param tint tint to be applied to [imageVector]. If [Color.Unspecified] is provided, then no tint
 *   is applied.
 * @param contentDescription text used by accessibility services to describe what this icon
 *   represents. This should always be provided unless this icon is used for decorative purposes,
 *   and does not represent a meaningful action that a user can take. This text should be localized,
 *   such as by using [androidx.compose.ui.res.stringResource] or similar
 * @param modifier the [Modifier] to be applied to this icon
 * @see LocalIconSize
 * @see GlimmerTheme.Companion.iconSizes
 */
@Composable
public fun Icon(
    imageVector: ImageVector,
    tint: Color,
    contentDescription: String?,
    modifier: Modifier = Modifier,
) {
    Icon(
        painter = rememberVectorPainter(imageVector),
        tint =
            if (tint.isSpecified) {
                { tint }
            } else null,
        contentDescription = contentDescription,
        modifier = modifier,
    )
}

/**
 * An icon component that draws [bitmap], with a default size of [LocalIconSize], and applies a
 * content color tint. Icon is an opinionated component designed to be used with single-color icons
 * so that they can be tinted correctly for the component they are placed in. The recommended icon
 * sizes can be retrieved using [GlimmerTheme.Companion.iconSizes]. A size can be set explicitly
 * using [size], and components can use [LocalIconSize] to set the preferred size for any Icons
 * inside the component. The content color used to tint this icon is provided by [surface]. To set a
 * custom tint color, use the Icon overload with a tint parameter. For multicolored icons and icons
 * that should not be tinted, use the overload and set [Color.Unspecified] as the tint color. For
 * generic images that should not be tinted, and should not use the provided icon size, use the
 * generic [androidx.compose.foundation.Image] instead.
 *
 * @param bitmap [ImageBitmap] to draw inside this icon
 * @param contentDescription text used by accessibility services to describe what this icon
 *   represents. This should always be provided unless this icon is used for decorative purposes,
 *   and does not represent a meaningful action that a user can take. This text should be localized,
 *   such as by using [androidx.compose.ui.res.stringResource] or similar
 * @param modifier the [Modifier] to be applied to this icon
 * @see LocalIconSize
 * @see GlimmerTheme.Companion.iconSizes
 */
@Composable
public fun Icon(bitmap: ImageBitmap, contentDescription: String?, modifier: Modifier = Modifier) {
    val painter = remember(bitmap) { BitmapPainter(bitmap) }
    Icon(painter = painter, contentDescription = contentDescription, modifier = modifier)
}

/**
 * An icon component that draws [bitmap], with a default size of [LocalIconSize], and applies a tint
 * of [tint]. Icon is an opinionated component designed to be used with single-color icons so that
 * they can be tinted correctly for the component they are placed in. The recommended icon sizes can
 * be retrieved using [GlimmerTheme.Companion.iconSizes]. A size can be set explicitly using [size],
 * and components can use [LocalIconSize] to set the preferred size for any Icons inside the
 * component. Use the other overload of Icon without a [tint] parameter to apply the recommended
 * content color provided by a [surface]. For multicolored icons and icons that should not be
 * tinted, set [tint] to [Color.Unspecified]. For generic images that should not be tinted, and
 * should not use the provided icon size, use the generic [androidx.compose.foundation.Image]
 * instead.
 *
 * @param bitmap [ImageBitmap] to draw inside this icon
 * @param tint tint to be applied to [bitmap]. If [Color.Unspecified] is provided, then no tint is
 *   applied.
 * @param contentDescription text used by accessibility services to describe what this icon
 *   represents. This should always be provided unless this icon is used for decorative purposes,
 *   and does not represent a meaningful action that a user can take. This text should be localized,
 *   such as by using [androidx.compose.ui.res.stringResource] or similar
 * @param modifier the [Modifier] to be applied to this icon
 * @see LocalIconSize
 * @see GlimmerTheme.Companion.iconSizes
 */
@Composable
public fun Icon(
    bitmap: ImageBitmap,
    tint: Color,
    contentDescription: String?,
    modifier: Modifier = Modifier,
) {
    val painter = remember(bitmap) { BitmapPainter(bitmap) }
    Icon(
        painter = painter,
        tint =
            if (tint.isSpecified) {
                { tint }
            } else null,
        contentDescription = contentDescription,
        modifier = modifier,
    )
}

/**
 * An icon component that draws [painter], with a default size of [LocalIconSize], and applies a
 * content color tint. Icon is an opinionated component designed to be used with single-color icons
 * so that they can be tinted correctly for the component they are placed in. The recommended icon
 * sizes can be retrieved using [GlimmerTheme.Companion.iconSizes]. A size can be set explicitly
 * using [size], and components can use [LocalIconSize] to set the preferred size for any Icons
 * inside the component. The content color used to tint this icon is provided by [surface]. To set a
 * custom tint color, use the Icon overload with a tint parameter. For multicolored icons and icons
 * that should not be tinted, use the overload and set `null` for the tint parameter. For generic
 * images that should not be tinted, and should not use the provided icon size, use the generic
 * [androidx.compose.foundation.Image] instead.
 *
 * @param painter [Painter] to draw inside this icon
 * @param contentDescription text used by accessibility services to describe what this icon
 *   represents. This should always be provided unless this icon is used for decorative purposes,
 *   and does not represent a meaningful action that a user can take. This text should be localized,
 *   such as by using [androidx.compose.ui.res.stringResource] or similar
 * @param modifier the [Modifier] to be applied to this icon
 * @see LocalIconSize
 * @see GlimmerTheme.Companion.iconSizes
 */
@Composable
public fun Icon(painter: Painter, contentDescription: String?, modifier: Modifier = Modifier) {
    Icon(
        painter = painter,
        useContentColor = true,
        tint = null,
        contentDescription = contentDescription,
        modifier = modifier,
    )
}

/**
 * An icon component that draws [painter], with a default size of [LocalIconSize], and applies a
 * tint of [tint]. Icon is an opinionated component designed to be used with single-color icons so
 * that they can be tinted correctly for the component they are placed in. The recommended icon
 * sizes can be retrieved using [GlimmerTheme.Companion.iconSizes]. A size can be set explicitly
 * using [size], and components can use [LocalIconSize] to set the preferred size for any Icons
 * inside the component. Use the other overload of Icon without a [tint] parameter to apply the
 * recommended content color provided by a [surface]. For multicolored icons and icons that should
 * not be tinted, set [tint] to `null`. For generic images that should not be tinted, and should not
 * use the provided icon size, use the generic [androidx.compose.foundation.Image] instead.
 *
 * @param painter [Painter] to draw inside this icon
 * @param tint tint to be applied to [painter]. If null, then no tint is applied.
 * @param contentDescription text used by accessibility services to describe what this icon
 *   represents. This should always be provided unless this icon is used for decorative purposes,
 *   and does not represent a meaningful action that a user can take. This text should be localized,
 *   such as by using [androidx.compose.ui.res.stringResource] or similar
 * @param modifier the [Modifier] to be applied to this icon
 * @see LocalIconSize
 * @see GlimmerTheme.Companion.iconSizes
 */
@Composable
public fun Icon(
    painter: Painter,
    tint: ColorProducer?,
    contentDescription: String?,
    modifier: Modifier = Modifier,
) {
    Icon(
        painter = painter,
        useContentColor = false,
        tint = tint,
        contentDescription = contentDescription,
        modifier = modifier,
    )
}

/**
 * CompositionLocal containing the preferred size of an icon. This value will be used by [Icon] by
 * default - it can be overridden with a [size] modifier.
 */
public val LocalIconSize: ProvidableCompositionLocal<Dp> =
    compositionLocalOf(structuralEqualityPolicy()) { GlimmerTheme._iconSizes.medium }

@Composable
private fun Icon(
    painter: Painter,
    useContentColor: Boolean,
    tint: ColorProducer?,
    contentDescription: String?,
    modifier: Modifier,
) {
    val colorFilter =
        if (useContentColor || tint != null) {
            IconColorFilterElement(
                useContentColor = useContentColor,
                tint = tint,
                painter = painter,
            )
        } else {
            Modifier
        }
    val semantics =
        if (contentDescription != null) {
            Modifier.semantics {
                this.contentDescription = contentDescription
                this.role = Role.Image
            }
        } else {
            Modifier
        }
    Box(
        modifier
            .toolingGraphicsLayer()
            .size(LocalIconSize.current)
            .then(colorFilter)
            .paint(painter, contentScale = ContentScale.Fit)
            .then(semantics)
    )
}

@Suppress("ModifierNodeInspectableProperties")
private class IconColorFilterElement(
    private val useContentColor: Boolean,
    private val tint: ColorProducer?,
    private val painter: Painter,
) : ModifierNodeElement<IconColorFilterNode>() {
    override fun create() = IconColorFilterNode(useContentColor, tint, painter)

    override fun update(node: IconColorFilterNode) {
        node.update(useContentColor, tint, painter)
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is IconColorFilterElement) return false

        if (useContentColor != other.useContentColor) return false
        if (tint != other.tint) return false
        if (painter != other.painter) return false

        return true
    }

    override fun hashCode(): Int {
        var result = useContentColor.hashCode()
        result = 31 * result + (tint?.hashCode() ?: 0)
        result = 31 * result + painter.hashCode()
        return result
    }
}

private class IconColorFilterNode(
    private var useContentColor: Boolean,
    private var tint: ColorProducer?,
    private var painter: Painter,
) : DelegatingNode() {
    val cacheDrawNode =
        delegate(
            CacheDrawModifierNode {
                val layer = obtainGraphicsLayer()
                layer.apply { record { drawContent() } }
                var cachedTintColor = Color.Unspecified
                var cachedColorFilter: ColorFilter? = null
                onDrawWithContent {
                    val tintColor =
                        if (useContentColor) {
                            currentContentColor()
                        } else {
                            tint?.invoke() ?: Color.Unspecified
                        }
                    if (cachedTintColor != tintColor) {
                        cachedTintColor = tintColor
                        cachedColorFilter =
                            if (tintColor.isSpecified) ColorFilter.tint(tintColor) else null
                        layer.colorFilter = cachedColorFilter
                    }
                    drawLayer(graphicsLayer = layer)
                }
            }
        )

    fun update(useContentColor: Boolean, tint: ColorProducer?, painter: Painter) {
        val painterChanged = this.painter != painter
        if (this.useContentColor != useContentColor || this.tint != tint || painterChanged) {
            this.useContentColor = useContentColor
            this.tint = tint
            this.painter = painter
            if (painterChanged) {
                cacheDrawNode.invalidateDrawCache()
            } else {
                cacheDrawNode.invalidateDraw()
            }
        }
    }
}