API source code for Surface

When using surfaces to build a custom component, refer to the following source code in Surface.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 android.graphics.RuntimeShader
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.FocusInteraction
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shader
import androidx.compose.ui.graphics.ShaderBrush
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.layout.layout
import androidx.compose.ui.node.DrawModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.invalidateDraw
import androidx.compose.ui.node.requireGraphicsContext
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.lerp
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.intellij.lang.annotations.Language

/**
 * A surface is a fundamental building block in Glimmer. A surface represents a distinct visual area
 * or 'physical' boundary for components such as buttons and cards. A [surface] implements shared
 * visual decoration for Jetpack Compose Glimmer components:
 * 1) Clipping: a surface clips its children to the shape specified by [shape]
 * 2) Border: a surface draws an inner [border] to emphasize the boundary of the component. When
 *    focused, a surface draws a wider border with a focused highlight on top to indicate the focus
 *    state.
 * 3) Background: a surface has a background color of [color].
 * 4) Depth effect: a surface can have different [DepthEffect] shadows for different states, as
 *    specified by [depthEffect].
 * 5) Content color: a surface provides a [contentColor] for text and icons inside the surface. By
 *    default this is calculated from the provided background color.
 * 6) Interaction states: when focused, a surface displays draws a wider border with a focused
 *    highlight on top. When pressed, a surface draws a pressed overlay. This happens for
 *    interactions emitted from [interactionSource].
 *
 * Use surface on its own for decorative elements that cannot be interacted with by a user:
 *
 * @sample androidx.xr.glimmer.samples.SurfaceSample
 *
 * In most cases surfaces should be interactive, to allow users to consistently move focus and
 * navigate between components. You can use [androidx.compose.foundation.focusable] for focus-only
 * surfaces, or [androidx.compose.foundation.clickable] and other modifiers for surfaces with
 * actions. To ensure the surface correctly reflects the interaction states, provide the same
 * [InteractionSource] to all modifiers.
 *
 * For example, to create a clickable surface:
 *
 * @sample androidx.xr.glimmer.samples.ClickableSurfaceSample
 *
 * Similarly, to create a focusable surface:
 *
 * @sample androidx.xr.glimmer.samples.FocusableSurfaceSample
 * @param enabled controls the enabled state of this surface. When `false`, a disabled overlay
 *   visual will be drawn on top of the surface. Note that this only affects the visual decoration;
 *   it does not intercept input or block interaction states (such as focus or press) from the
 *   [interactionSource].
 * @param shape the [Shape] used to clip this surface, and also used to draw the background and
 *   border
 * @param color the background [Color] for this surface
 * @param contentColor the [Color] for content inside this surface
 * @param depthEffect the [SurfaceDepthEffect] for this surface, representing the [DepthEffect]
 *   shadows rendered in different states.
 * @param border an optional inner border for this surface
 * @param interactionSource the [InteractionSource] that emits [Interaction]s for this surface. For
 *   interactive surfaces, the [InteractionSource] instance provided to this surface must be shared
 *   with the modifier responsible for emitting [Interaction]s, such as
 *   [androidx.compose.foundation.focusable] or [androidx.compose.foundation.clickable].
 */
@Composable
public fun Modifier.surface(
    enabled: Boolean = true,
    shape: Shape = GlimmerTheme.shapes.medium,
    color: Color = GlimmerTheme.colors.surface,
    contentColor: Color = calculateContentColor(color),
    depthEffect: SurfaceDepthEffect? = null,
    border: BorderStroke? = SurfaceDefaults.border(),
    interactionSource: InteractionSource? = null,
): Modifier {
    return this.surfaceDepthEffect(depthEffect, shape, interactionSource)
        .clip(shape)
        .contentColorProvider(contentColor)
        .then(
            SurfaceNodeElement(
                enabled = enabled,
                shape = shape,
                border = border,
                interactionSource = interactionSource,
            )
        )
        .background(color = color, shape = shape)
}

/**
 * Represents the [DepthEffect] used by a [surface] in different states.
 *
 * Focused [surface]s with a [focusedDepthEffect] will have a higher zIndex set so they can draw
 * their focused depth effect over siblings.
 *
 * @property [depthEffect] the [DepthEffect] used when the [surface] is in its default state (no
 *   other interactions are ongoing)
 * @property [focusedDepthEffect] the [DepthEffect] used when the [surface] is focused
 */
@Immutable
public class SurfaceDepthEffect(
    public val depthEffect: DepthEffect?,
    public val focusedDepthEffect: DepthEffect?,
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is SurfaceDepthEffect) return false

        if (depthEffect != other.depthEffect) return false
        if (focusedDepthEffect != other.focusedDepthEffect) return false

        return true
    }

    override fun hashCode(): Int {
        var result = depthEffect?.hashCode() ?: 0
        result = 31 * result + (focusedDepthEffect?.hashCode() ?: 0)
        return result
    }
}

/** Default values used for [surface]. */
public object SurfaceDefaults {
    /**
     * Create the default [BorderStroke] used for a [surface]. Use the other overload in order to
     * change the width or color.
     */
    @Composable
    public fun border(): BorderStroke {
        return GlimmerTheme.LocalGlimmerTheme.current.defaultSurfaceBorder
    }

    /**
     * Create the default [BorderStroke] used for a [surface], with optional overrides for [width]
     * and [color].
     *
     * @param width width of the border in [Dp]. Use [Dp.Hairline] for one-pixel border.
     * @param color color to paint the border with
     */
    @Composable
    public fun border(
        width: Dp = DefaultSurfaceBorderWidth,
        color: Color = GlimmerTheme.colors.outline,
    ): BorderStroke {
        return BorderStroke(width, color)
    }

    /** Returns the default (cached) border for a [surface]. */
    internal val GlimmerTheme.defaultSurfaceBorder: BorderStroke
        get() {
            return defaultSurfaceBorderCached
                ?: BorderStroke(DefaultSurfaceBorderWidth, colors.outline).also {
                    defaultSurfaceBorderCached = it
                }
        }
}

/**
 * Surface node responsible for drawing the border, focused border and highlight, and pressed
 * overlay.
 */
private class SurfaceNodeElement(
    private val enabled: Boolean,
    private val shape: Shape,
    private val border: BorderStroke?,
    private val interactionSource: InteractionSource?,
) : ModifierNodeElement<SurfaceNode>() {
    override fun create(): SurfaceNode =
        SurfaceNode(
            enabled = enabled,
            shape = shape,
            border = border,
            interactionSource = interactionSource,
        )

    override fun update(node: SurfaceNode) =
        node.update(
            enabled = enabled,
            shape = shape,
            border = border,
            interactionSource = interactionSource,
        )

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

        if (enabled != other.enabled) return false
        if (shape != other.shape) return false
        if (border != other.border) return false
        if (interactionSource != other.interactionSource) return false

        return true
    }

    override fun hashCode(): Int {
        var result = enabled.hashCode()
        result = 31 * result + shape.hashCode()
        result = 31 * result + (border?.hashCode() ?: 0)
        result = 31 * result + (interactionSource?.hashCode() ?: 0)
        return result
    }

    override fun InspectorInfo.inspectableProperties() {
        name = "surface"
        properties["enabled"] = enabled
        properties["shape"] = shape
        properties["border"] = border
        properties["interactionSource"] = interactionSource
    }
}

private class SurfaceNode(
    private var enabled: Boolean,
    private var shape: Shape,
    private var border: BorderStroke?,
    private var interactionSource: InteractionSource?,
) : DrawModifierNode, Modifier.Node() {

    override val shouldAutoInvalidate = false

    // Cache borders and highlight for unfocused and focused states. This means we
    // can avoid recreating these for a given surface, if the border and shape never
    // change. Changing between unfocused and focus states only requires a draw
    // invalidation, as the borders are already cached.

    // Unfocused border
    private val unfocusedBorderLogic: BorderLogic = BorderLogic()
    private val unfocusedBorderWidth: () -> Dp = { border?.width ?: 0.dp }
    // Focused border - this consists of two layers. A 'base' layer (which is the
    // unfocused border with a different size) and the highlight we draw on top of
    // this base layer. We need to increase the size of the underlying border to
    // make sure that the highlight area fully matches the underlying border, to
    // avoid inconsistent areas of coverage due to the transparency of the
    // highlight.
    private var focusedBorderLogic: BorderLogic? = null
    private var focusedHighlightBorderLogic: BorderLogic? = null
    private var focusedBorderWidth: (() -> Dp)? = null

    // Highlight shader / brush
    var shader: Shader? = null
    var shaderBrush: Brush? = null

    // Graphics Layer
    private var unfocusedBorderLayer: GraphicsLayer? = null
    private var unfocusedGraphicsLayerProvider: (() -> GraphicsLayer)? = null

    private var focusedBorderLayer: GraphicsLayer? = null
    private var focusedGraphicsLayerProvider: (() -> GraphicsLayer)? = null

    private var focusedHighlightBorderLayer: GraphicsLayer? = null
    private var focusedHighlightGraphicsLayerProvider: (() -> GraphicsLayer)? = null

    private var interactionCollectionJob: Job? = null

    // Enter / exit animation progress for the width and fade effect applied to the highlight
    private var _focusedHighlightProgress: Animatable<Float, AnimationVector1D>? = null
    private val focusedHighlightProgress
        get() = _focusedHighlightProgress?.value ?: 0f

    // Rotation progress applied to the highlight
    private var _focusedHighlightRotationProgress: Animatable<Float, AnimationVector1D>? = null
    private val focusedHighlightRotationProgress
        get() = _focusedHighlightRotationProgress?.value ?: 0f

    private var pressedOverlayAlpha: Animatable<Float, AnimationVector1D>? = null
    // Job that runs for a minimum duration to make sure quick presses are still visible
    private var minimumPressDuration: Job? = null
    private var pressReleaseAnimation: Job? = null

    fun update(
        enabled: Boolean,
        shape: Shape,
        border: BorderStroke?,
        interactionSource: InteractionSource?,
    ) {
        if (this.enabled != enabled) {
            this.enabled = enabled
            invalidateDraw()
        }
        if (this.shape != shape) {
            this.shape = shape
            invalidateDraw()
        }
        if (this.border != border) {
            this.border = border
            invalidateDraw()
        }
        if (this.interactionSource != interactionSource) {
            this.interactionSource = interactionSource
            observeInteractions()
        }
    }

    override fun onAttach() {
        observeInteractions()
    }

    var isFocused = false
        set(value) {
            if (field != value) {
                field = value
                if (value) {
                    startFocusAnimation()
                } else {
                    stopFocusAnimation()
                }
                // No need to invalidate the border cache - we build it ahead of time to account for
                // focus changes. Just invalidate draw so we can switch to drawing the correct
                // border.
                invalidateDraw()
            }
        }

    var isPressed = false
        set(value) {
            if (field != value) {
                field = value
                if (value) {
                    pressedOverlayAlpha = pressedOverlayAlpha ?: Animatable(0f)
                    pressReleaseAnimation?.cancel()
                    minimumPressDuration?.cancel()

                    minimumPressDuration =
                        coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
                            delay(PressedOverlayMinimumDurationMillis)
                        }
                    coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
                        pressedOverlayAlpha?.animateTo(
                            PressedOverlayAlpha,
                            PressedOverlayEnterAnimationSpec,
                        )
                    }
                } else {
                    pressedOverlayAlpha?.let { progress ->
                        pressReleaseAnimation =
                            coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
                                minimumPressDuration?.join()
                                progress.animateTo(0f, PressedOverlayExitAnimationSpec)
                            }
                    }
                }
                invalidateDraw()
            }
        }

    private fun observeInteractions() {
        interactionCollectionJob?.cancel()
        interactionCollectionJob = null
        isFocused = false
        isPressed = false
        interactionSource?.let { source ->
            interactionCollectionJob =
                coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
                    var focusCount = 0
                    var pressCount = 0
                    source.interactions.collect { interaction ->
                        when (interaction) {
                            is FocusInteraction.Focus -> focusCount++
                            is FocusInteraction.Unfocus -> focusCount--
                            is PressInteraction.Press -> pressCount++
                            is PressInteraction.Release -> pressCount--
                            is PressInteraction.Cancel -> pressCount--
                        }
                        isFocused = focusCount > 0
                        isPressed = pressCount > 0
                    }
                }
        }
    }

    private fun startFocusAnimation() {
        _focusedHighlightProgress = _focusedHighlightProgress ?: Animatable(0f)
        _focusedHighlightRotationProgress = _focusedHighlightRotationProgress ?: Animatable(0f)
        coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
            _focusedHighlightProgress?.snapTo(0f)
            _focusedHighlightProgress?.animateTo(
                targetValue = 1f,
                animationSpec = FocusedEnterAnimationSpec,
            )
        }
        coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
            _focusedHighlightRotationProgress?.snapTo(0f)
            _focusedHighlightRotationProgress?.animateTo(
                targetValue = 1f,
                animationSpec = FocusedHighlightRotationAnimationSpec,
            )
        }
    }

    private fun stopFocusAnimation() {
        coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
            _focusedHighlightProgress?.animateTo(
                targetValue = 0f,
                animationSpec = FocusedExitAnimationSpec,
            )
            if (isActive) {
                _focusedHighlightRotationProgress?.snapTo(0f)
            }
        }
    }

    override fun ContentDrawScope.draw() {
        val outline = shape.createOutline(size, layoutDirection, this)
        drawContent()
        val pressedOverlayColor = PressedOverlayColor.copy(alpha = pressedOverlayAlpha?.value ?: 0f)
        drawRect(color = pressedOverlayColor)
        if (border != null) {
            val progress = focusedHighlightProgress
            val gContext = requireGraphicsContext()
            if (progress > 0f) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                    val rotationProgressRadians =
                        (FocusedHighlightRotationEndAngleRadians -
                            FocusedHighlightRotationStartAngleRadians) *
                            focusedHighlightRotationProgress
                    val rotationRadians =
                        FocusedHighlightRotationStartAngleRadians + rotationProgressRadians
                    shader =
                        HighlightShaderHelper.configureShader(
                            shader = shader,
                            size = size,
                            rotationRadians = rotationRadians.toFloat(),
                            progress = progress,
                        )
                    shaderBrush = shaderBrush ?: ShaderBrush(shader!!)
                }
                focusedBorderLogic = focusedBorderLogic ?: BorderLogic()
                focusedHighlightBorderLogic = focusedHighlightBorderLogic ?: BorderLogic()
                focusedBorderWidth =
                    focusedBorderWidth
                        ?: {
                            val b = border
                            if (b != null) {
                                lerp(
                                    b.width,
                                    FocusedSurfaceBorderWidth,
                                    // Capture class property instead of function-local progress to
                                    // make sure this will read the animation state when the lambda
                                    // is invoked and not capture a stale variable
                                    focusedHighlightProgress,
                                )
                            } else {
                                0.dp
                            }
                        }
                focusedBorderLogic!!.drawBorder(
                    this,
                    focusedBorderWidth!!,
                    border!!.brush,
                    focusedGraphicsLayerProvider
                        ?: {
                                focusedBorderLayer
                                    ?: gContext.createGraphicsLayer().also {
                                        focusedBorderLayer = it
                                    }
                            }
                            .also { focusedGraphicsLayerProvider = it },
                    outline,
                )

                shaderBrush?.let {
                    focusedHighlightBorderLogic!!.drawBorder(
                        this,
                        focusedBorderWidth!!,
                        it,
                        focusedHighlightGraphicsLayerProvider
                            ?: {
                                    focusedHighlightBorderLayer
                                        ?: gContext.createGraphicsLayer().also {
                                            focusedHighlightBorderLayer = it
                                        }
                                }
                                .also { focusedHighlightGraphicsLayerProvider = it },
                        outline,
                    )
                }
            } else {
                unfocusedBorderLogic.drawBorder(
                    this,
                    unfocusedBorderWidth,
                    border!!.brush,
                    unfocusedGraphicsLayerProvider
                        ?: {
                                unfocusedBorderLayer
                                    ?: gContext.createGraphicsLayer().also {
                                        unfocusedBorderLayer = it
                                    }
                            }
                            .also { unfocusedGraphicsLayerProvider = it },
                    outline,
                )
            }
        }
        if (!enabled) {
            drawRect(DisabledOverlayColor)
        }
    }

    override fun onDetach() {
        _focusedHighlightProgress = null
        _focusedHighlightRotationProgress = null
        pressedOverlayAlpha = null

        unfocusedGraphicsLayerProvider = null
        val gContext = requireGraphicsContext()
        unfocusedBorderLayer?.let {
            gContext.releaseGraphicsLayer(it)
            unfocusedBorderLayer = null
        }

        focusedGraphicsLayerProvider = null
        focusedBorderLayer?.let {
            gContext.releaseGraphicsLayer(it)
            focusedBorderLayer = null
        }

        focusedHighlightGraphicsLayerProvider = null
        focusedHighlightBorderLayer?.let {
            gContext.releaseGraphicsLayer(it)
            focusedHighlightBorderLayer = null
        }
    }
}

/**
 * Renders and animates a [surface]'s [depthEffect] for a given [shape], by observing
 * [interactionSource].
 */
@Composable
private fun Modifier.surfaceDepthEffect(
    depthEffect: SurfaceDepthEffect?,
    shape: Shape,
    interactionSource: InteractionSource?,
): Modifier {
    if (depthEffect == null) return this
    val focusedProgress = remember { Animatable(0f) }
    // If focused and there is focused depth effect, we need to draw the surface on top of
    // other siblings to make sure the depth effect occludes siblings.
    val zIndex by remember {
        // Derived to avoid invalidating layout each frame of the animation
        derivedStateOf {
            if (depthEffect.focusedDepthEffect != null && focusedProgress.value >= 0.5f) 1f else 0f
        }
    }
    if (interactionSource != null) {
        LaunchedEffect(interactionSource) {
            interactionSource.interactions.collect { interaction ->
                when (interaction) {
                    is FocusInteraction.Focus ->
                        launch(start = CoroutineStart.UNDISPATCHED) {
                            focusedProgress.animateTo(1f, FocusedEnterAnimationSpec)
                        }

                    is FocusInteraction.Unfocus ->
                        launch(start = CoroutineStart.UNDISPATCHED) {
                            focusedProgress.animateTo(0f, FocusedExitAnimationSpec)
                        }
                }
            }
        }
    }

    return layout { measurable, constraints ->
            val placeable = measurable.measure(constraints)
            layout(placeable.width, placeable.height) { placeable.place(0, 0, zIndex = zIndex) }
        }
        .depthEffect(
            from = depthEffect.depthEffect,
            to = depthEffect.focusedDepthEffect,
            shape = shape,
            progress = { focusedProgress.value },
        )
}

/** Default border width for a [surface]. */
private val DefaultSurfaceBorderWidth = 2.dp

/** Focused border width for a [surface]. */
private val FocusedSurfaceBorderWidth = 5.dp

/** Enter animation for focus highlight and depth effect */
private val FocusedEnterAnimationSpec: AnimationSpec<Float> =
    spring(dampingRatio = 1f, stiffness = 600f)

/** Exit animation for focus highlight and depth effect */
private val FocusedExitAnimationSpec: AnimationSpec<Float> =
    spring(dampingRatio = 1f, stiffness = 100f)

private val FocusedHighlightRotationAnimationSpec: AnimationSpec<Float> =
    tween(durationMillis = 700, easing = LinearOutSlowInEasing, delayMillis = 40)

private val FocusedHighlightRotationStartAngleRadians: Double = Math.toRadians(-100.0)

private val FocusedHighlightRotationEndAngleRadians: Double = Math.toRadians(35.0)

private val PressedOverlayColor = Color.White

internal val DisabledOverlayColor = Color(0x8F191919)

private const val PressedOverlayAlpha = 0.16f

private val PressedOverlayEnterAnimationSpec: AnimationSpec<Float> =
    spring(dampingRatio = 0.84f, stiffness = 8000f)

private val PressedOverlayExitAnimationSpec: AnimationSpec<Float> =
    spring(dampingRatio = 0.85f, stiffness = 50f)

private const val PressedOverlayMinimumDurationMillis = 300L

@Language(value = "AGSL")
private const val FocusedHighlightShader =
    """
/**
 * Rotating linear gradient shader, where rotation is controlled by iRotation uniform.
 * This is essentially the same as:
 * LinearGradientShader(
 *     colors = FocusedHighlightColors,
 *     colorStops = FocusedHighlightColorStops,
 *     from = Offset.Zero,
 *     to = Offset(size.width, size.height),
 * )
 * But allowing for efficient rotation, instead of needing to create a new shader / brush every
 * frame with new coordinates.
 */
// Width / height
uniform float2 iResolution;
// Rotation in radians. 0 radians means a horizontal gradient.
// Positive values will have the effect of rotating the gradient clockwise.
uniform float iRotation;
// Alpha animation progress from 0 to 1. This will be applied to the color stops so that each
// color stop will fade in.
uniform float iAlphaProgress;

half4 main(float2 fragCoord) {
    // Horizontal gradient
    half4 colors[4];
    colors[0] = half4(1.0, 1.0, 1.0, 1.0 * iAlphaProgress); // White with 100% alpha
    colors[1] = half4(1.0, 1.0, 1.0, 0.2 * iAlphaProgress); // White with 20% alpha
    colors[2] = half4(1.0, 1.0, 1.0, 0.2 * iAlphaProgress); // White with 20% alpha
    colors[3] = half4(1.0, 1.0, 1.0, 0.8 * iAlphaProgress); // White with 80% alpha

    // Stops for the horizontal gradient
    float stops[4];
    stops[0] = 0.0;
    stops[1] = 0.3;
    stops[2] = 0.66;
    stops[3] = 1.0;

    // Normalize
    half2 uv = fragCoord.xy / iResolution.xy;

    // Offset around a rotational center
    half2 rotationCenter = half2(0.5, 0.5);
    uv -= rotationCenter;

    // Rotate
    // We rotate in the opposite direction as we are rotating the coordinate we sample the gradient
    // from. To create the effect of a gradient 'moving' clockwise, we need to move the
    // coordinate in the opposite direction (counter-clockwise).
    float2x2 matrix = float2x2(cos(-iRotation),-sin(-iRotation),sin(-iRotation),cos(-iRotation));
    uv *= matrix;

    // Translate back into [0,1] space
    uv += rotationCenter;

    // Blend through stops using the x coordinate, since we have a horizontal gradient
    half4 color = mix(colors[0], colors[1], smoothstep(stops[0], stops[1], uv.x));
    color = mix(color, colors[2], smoothstep(stops[1], stops[2], uv.x));
    color = mix(color, colors[3], smoothstep(stops[2], stops[3], uv.x));

    return color;
}
"""

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private object HighlightShaderHelper {
    @JvmStatic
    fun configureShader(
        shader: Shader?,
        size: Size,
        rotationRadians: Float,
        progress: Float,
    ): Shader {
        val shader = shader as? RuntimeShader ?: RuntimeShader(FocusedHighlightShader)
        shader.setFloatUniform("iResolution", size.width, size.height)
        shader.setFloatUniform("iRotation", rotationRadians)
        shader.setFloatUniform("iAlphaProgress", progress)
        return shader
    }
}