Placeable.kt

/*
 * Copyright 2019 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

import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.round

/**
 * A [Placeable] corresponds to a child layout that can be positioned by its
 * parent layout. Most [Placeable]s are the result of a [Measurable.measure] call.
 *
 * A `Placeable` should never be stored between measure calls.
 */
abstract class Placeable {
    /**
     * The width, in pixels, of the measured layout, as seen by the parent. This is usually the
     * `width` value passed into [MeasureScope.layout], but will be different if the layout does
     * not respect its incoming constraints, so the width will be coerced inside the min and
     * max width.
     */
    var width: Int = 0
        private set

    /**
     * The height, in pixels, of the measured layout, as seen by the parent. This is usually the
     * `height` value passed into [MeasureScope.layout], but can be different if the layout does
     * not respect its incoming constraints, so the height will be coerced inside the min and
     * max height.
     */
    var height: Int = 0
        private set

    /**
     * Returns the position of an [alignment line][AlignmentLine],
     * or `null` if the line is not provided.
     */
    abstract operator fun get(line: AlignmentLine): Int

    /**
     * The measured size of this Placeable. This might not respect [measurementConstraints].
     */
    protected var measuredSize: IntSize = IntSize(0, 0)
        set(value) {
            field = value
            width = value.width.coerceIn(
                measurementConstraints.minWidth,
                measurementConstraints.maxWidth
            )
            height = value.height.coerceIn(
                measurementConstraints.minHeight,
                measurementConstraints.maxHeight
            )
        }

    internal val measuredWidth get() = measuredSize.width

    internal val measuredHeight get() = measuredSize.height

    /**
     * Positions the [Placeable] at [position] in its parent's coordinate system.
     */
    protected abstract fun placeAt(position: IntOffset)

    /**
     * The constraints used for the measurement made to obtain this [Placeable].
     */
    protected var measurementConstraints: Constraints = Constraints()

    /**
     * The offset to be added to an apparent position assigned to this [Placeable] to make it real.
     * The real layout will be centered on the space assigned by the parent, which computed the
     * child's position only seeing its apparent size.
     */
    protected val apparentToRealOffset: IntOffset
        get() = IntOffset((width - measuredSize.width) / 2, (height - measuredSize.height) / 2)

    /**
     * Receiver scope that permits explicit placement of a [Placeable].
     *
     * While a [Placeable] may be placed at any time, this explicit receiver scope is used
     * to discourage placement outside of [MeasureScope.layout] positioning blocks.
     * This permits Compose UI to perform additional layout optimizations allowing repositioning
     * a [Placeable] without remeasuring its original [Measurable] if factors contributing to its
     * potential measurement have not changed.
     * The scope also allows automatic mirroring of children positions in RTL layout direction
     * contexts using the [placeRelative] methods available in the scope. If the automatic
     * mirroring is not desired, [place] should be used instead.
     */
    // TODO(b/150276678): using the PlacementScope to place outside the layout pass is not working.
    abstract class PlacementScope {
        /**
         * Keeps the parent layout node's width to make the automatic mirroring of the position
         * in RTL environment. If the value is zero, than the [Placeable] will be be placed to
         * the original position (position will not be mirrored).
         */
        protected abstract val parentWidth: Int

        /**
         * Keeps the layout direction of the parent of the placeable that is being places using
         * current [PlacementScope]. Used to support automatic position mirroring for convenient
         * RTL support in custom layouts.
         */
        protected abstract val parentLayoutDirection: LayoutDirection

        /**
         * Place a [Placeable] at [position] in its parent's coordinate system.
         * If the layout direction is right-to-left, the given [position] will be horizontally
         * mirrored so that the position of the [Placeable] implicitly reacts to RTL layout
         * direction contexts.
         * If this method is used outside the [MeasureScope.layout] positioning block, the
         * automatic position mirroring will not happen and the [Placeable] will be placed at the
         * given [position], similar to the [place] method.
         */
        fun Placeable.placeRelative(position: IntOffset) = placeAutoMirrored(position)

        /**
         * Place a [Placeable] at [position] in its parent's coordinate system.
         * If the layout direction is right-to-left, the given [position] will be horizontally
         * mirrored so that the position of the [Placeable] implicitly reacts to RTL layout
         * direction contexts.
         * If this method is used outside the [MeasureScope.layout] positioning block, the
         * automatic position mirroring will not happen and the [Placeable] will be placed at the
         * given [position], similar to the [place] method.
         */
        fun Placeable.placeRelative(position: Offset) = placeAutoMirrored(position.round())

        /**
         * Place a [Placeable] at [x], [y] in its parent's coordinate system.
         * If the layout direction is right-to-left, the given position will be horizontally
         * mirrored so that the position of the [Placeable] implicitly reacts to RTL layout
         * direction contexts.
         * If this method is used outside the [MeasureScope.layout] positioning block, the
         * automatic position mirroring will not happen and the [Placeable] will be placed at the
         * given position, similar to the [place] method.
         */
        fun Placeable.placeRelative(x: Int, y: Int) = placeAutoMirrored(IntOffset(x, y))

        /**
         * Place a [Placeable] at [position] in its parent's coordinate system.
         * Unlike [placeRelative], the given [position] will not implicitly react in RTL layout direction
         * contexts.
         */
        fun Placeable.place(position: Offset) = place(position.round())

        /**
         * Place a [Placeable] at [x], [y] in its parent's coordinate system.
         * Unlike [placeRelative], the given position will not implicitly react in RTL layout direction
         * contexts.
         */
        fun Placeable.place(x: Int, y: Int) = place(IntOffset(x, y))

        /**
         * Place a [Placeable] at [position] in its parent's coordinate system.
         * Unlike [placeRelative], the given [position] will not implicitly react in RTL layout direction
         * contexts.
         */
        fun Placeable.place(position: IntOffset) =
            placeAt(position + apparentToRealOffset)

        private fun Placeable.placeAutoMirrored(position: IntOffset) {
            if (parentLayoutDirection == LayoutDirection.Ltr || parentWidth == 0) {
                place(position)
            } else {
                place(IntOffset(parentWidth - measuredSize.width - position.x, position.y))
            }
        }

        internal companion object : PlacementScope() {
            override var parentLayoutDirection = LayoutDirection.Ltr
                private set
            override var parentWidth = 0
                private set

            inline fun executeWithRtlMirroringValues(
                parentWidth: Int,
                parentLayoutDirection: LayoutDirection,
                crossinline block: PlacementScope.() -> Unit
            ) {
                val previousParentWidth = this.parentWidth
                val previousParentLayoutDirection = this.parentLayoutDirection
                this.parentWidth = parentWidth
                this.parentLayoutDirection = parentLayoutDirection
                this.block()
                this.parentWidth = previousParentWidth
                this.parentLayoutDirection = previousParentLayoutDirection
            }
        }
    }
}