LayoutNodeAlignmentLines.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.compose.ui.node

import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.HorizontalAlignmentLine
import androidx.compose.ui.layout.merge
import kotlin.math.roundToInt

internal class LayoutNodeAlignmentLines(
    private val layoutNode: LayoutNode
) {
    /**
     * `true` when the alignment lines needs to be recalculated because they might have changed.
     */
    internal var dirty = true

    /**
     * `true` when the alignment lines were used by the parent during measurement.
     */
    internal var usedDuringParentMeasurement = false

    /**
     * `true` when the alignment lines have been used by the parent during the current layout (or
     * previous layout if there is no layout in progress).
     */
    internal var usedDuringParentLayout = false
    /**
     * `true` when the alignment lines were used by the parent during the last completed layout.
     */
    internal var previousUsedDuringParentLayout = false

    /**
     * `true` when the alignment lines were used by the modifier of the node during measurement.
     */
    internal var usedByModifierMeasurement = false

    /**
     * `true` when the alignment lines were used by the modifier of the node during measurement.
     */
    internal var usedByModifierLayout = false

    /**
     * `true` when the direct parent or our modifier relies on our alignment lines.
     */
    internal val queried get() = usedDuringParentMeasurement ||
        previousUsedDuringParentLayout || usedByModifierMeasurement ||
        usedByModifierLayout

    /**
     * The closest layout node ancestor who was asked for alignment lines, either by the parent or
     * their own modifier. If the owner stops being queried for alignment lines, we have to
     * [recalculateQueryOwner] to find the new owner if one exists.
     */
    private var queryOwner: LayoutNode? = null

    /**
     * Whether the alignment lines of this node are relevant (whether an ancestor depends on them).
     */
    internal val required: Boolean get() {
        recalculateQueryOwner()
        return queryOwner != null
    }

    /**
     * Updates the alignment lines query owner according to the current values of the
     * alignmentUsedBy* of the layout nodes in the hierarchy.
     */
    internal fun recalculateQueryOwner() {
        queryOwner = if (queried) {
            layoutNode
        } else {
            val parent = layoutNode.parent ?: return
            val parentQueryOwner = parent.alignmentLines.queryOwner
            if (parentQueryOwner != null && parentQueryOwner.alignmentLines.queried) {
                parentQueryOwner
            } else {
                val owner = queryOwner
                if (owner == null || owner.alignmentLines.queried) return
                owner.parent?.alignmentLines?.recalculateQueryOwner()
                owner.parent?.alignmentLines?.queryOwner
            }
        }
    }

    /**
     * The alignment lines of this layout, inherited + intrinsic
     */
    private val alignmentLines: MutableMap<AlignmentLine, Int> = hashMapOf()

    fun getLastCalculation(): Map<AlignmentLine, Int> = alignmentLines

    fun recalculate() {
        alignmentLines.clear()
        /**
         * Returns the alignment line value for a given alignment line without affecting whether
         * the flag for whether the alignment line was read.
         */
        fun addAlignmentLine(
            alignmentLine: AlignmentLine,
            initialPosition: Int,
            initialWrapper: LayoutNodeWrapper
        ) {
            var position = Offset(initialPosition.toFloat(), initialPosition.toFloat())
            var wrapper = initialWrapper
            while (true) {
                position = wrapper.toParentPosition(position)
                wrapper = wrapper.wrappedBy!!
                if (wrapper == layoutNode.innerLayoutNodeWrapper) break
                if (alignmentLine in wrapper.providedAlignmentLines) {
                    val newPosition = wrapper[alignmentLine]
                    position = Offset(newPosition.toFloat(), newPosition.toFloat())
                }
            }
            val positionInContainer = if (alignmentLine is HorizontalAlignmentLine) {
                position.y.roundToInt()
            } else {
                position.x.roundToInt()
            }
            // If the line was already provided by a previous child, merge the values.
            alignmentLines[alignmentLine] = if (alignmentLine in alignmentLines) {
                alignmentLine.merge(
                    alignmentLines.getValue(alignmentLine),
                    positionInContainer
                )
            } else {
                positionInContainer
            }
        }
        layoutNode._children.forEach { child ->
            if (!child.isPlaced) return@forEach
            if (child.alignmentLines.dirty) {
                // It did not need relayout, but we still call layout to recalculate
                // alignment lines.
                child.layoutChildren()
            }
            // Add alignment lines on the child node.
            child.alignmentLines.alignmentLines.forEach { (childLine, linePosition) ->
                addAlignmentLine(childLine, linePosition, child.innerLayoutNodeWrapper)
            }

            // Add alignment lines on the modifier of the child.
            var wrapper = child.innerLayoutNodeWrapper.wrappedBy!!
            while (wrapper != layoutNode.innerLayoutNodeWrapper) {
                wrapper.providedAlignmentLines.forEach { childLine ->
                    addAlignmentLine(childLine, wrapper[childLine], wrapper)
                }
                wrapper = wrapper.wrappedBy!!
            }
        }
        alignmentLines += layoutNode.innerLayoutNodeWrapper.measureResult.alignmentLines
        dirty = false
    }

    internal fun reset() {
        dirty = true
        usedDuringParentMeasurement = false
        previousUsedDuringParentLayout = false
        usedDuringParentLayout = false
        usedByModifierMeasurement = false
        usedByModifierLayout = false
        queryOwner = null
    }
}