PointerEvent.kt

/*
 * Copyright 2020 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.
 */

@file:Suppress("EXPERIMENTAL_FEATURE_WARNING")

package androidx.compose.ui.input.pointer

import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerEventPass.Final
import androidx.compose.ui.input.pointer.PointerEventPass.Initial
import androidx.compose.ui.input.pointer.PointerEventPass.Main
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.node.InternalCoreApi
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.Uptime
import androidx.compose.ui.unit.round

/**
 * A [Modifier.Element] that can interact with pointer input.
 */
interface PointerInputModifier : Modifier.Element {
    val pointerInputFilter: PointerInputFilter
}

/**
 * A PointerInputFilter represents a single entity that receives [PointerInputChange]s),
 * interprets them, and consumes the aspects of the changes that it is react to such that other
 * PointerInputFilters don't also react to them.
 */
abstract class PointerInputFilter {

    /**
     * Invoked when pointers that previously hit this [PointerInputFilter] have changed.
     *
     * @param pointerEvent The list of [PointerInputChange]s with positions relative to this
     * [PointerInputFilter].
     * @param pass The [PointerEventPass] in which this function is being called.
     * @param bounds The width and height associated with this [PointerInputFilter].
     * @return The list of [PointerInputChange]s after any aspect of the changes have been consumed.
     *
     * @see PointerInputChange
     * @see PointerEventPass
     */
    abstract fun onPointerEvent(
        pointerEvent: PointerEvent,
        pass: PointerEventPass,
        bounds: IntSize
    ): List<PointerInputChange>

    /**
     * Invoked to notify the handler that no more calls to [PointerInputFilter] will be made, until
     * at least new pointers exist.  This can occur for a few reasons:
     * 1. Android dispatches ACTION_CANCEL to Compose.
     * 2. This [PointerInputFilter] is no longer associated with a LayoutNode.
     * 3. This [PointerInputFilter]'s associated LayoutNode is no longer in the composition tree.
     */
    abstract fun onCancel()

    /**
     * Invoked right after this [PointerInputFilter] is hit by a pointer during hit testing.
     *
     * @param customEventDispatcher The [CustomEventDispatcher] that can be used to dispatch
     * [CustomEvent] across the tree of hit [PointerInputFilter]s.
     *
     * @See CustomEventDispatcher
     */
    open fun onInit(customEventDispatcher: CustomEventDispatcher) {}

    /**
     * Invoked when a [CustomEvent] is dispatched by a [PointerInputFilter].
     *
     * Dispatch occurs over all passes of [PointerEventPass].
     *
     * @param customEvent The [CustomEvent] is the event being dispatched.
     * @param pass The [PointerEventPass] in which this function is being called.
     *
     * @see CustomEvent
     * @see PointerEventPass
     */
    open fun onCustomEvent(customEvent: CustomEvent, pass: PointerEventPass) {}

    internal var layoutCoordinates: LayoutCoordinates? = null

    /**
     * The layout size assigned to this [PointerInputFilter].
     */
    val size: IntSize
        get() = layoutCoordinates?.size ?: IntSize.Zero
    internal val position: IntOffset
        get() = layoutCoordinates?.run { localToGlobal(Offset.Zero).round() } ?: IntOffset.Zero
    internal val isAttached: Boolean
        get() = layoutCoordinates?.isAttached == true
}

/**
 * Describes a pointer input change event that has occurred at a particular point in time.
 */
expect class PointerEvent @OptIn(InternalCoreApi::class) internal constructor(
    changes: List<PointerInputChange>,
    internalPointerEvent: InternalPointerEvent?
) {
    /**
     * @param changes The changes.
     */
    constructor(changes: List<PointerInputChange>)

    /**
     * The changes.
     */
    val changes: List<PointerInputChange>
}

/**
 * Describes a change that has occurred for a particular pointer, as well as how much of the change
 * has been consumed (meaning, used by a node in the UI).
 *
 * The [current] data always represents the position of the pointer relative to the element that
 * this [PointerInputChange] is being dispatched to.
 *
 * The [previous] data, however, represents the position of the pointer offset to the current
 * position of the pointer relative to the screen.
 *
 * This means that [current] and [previous] can always be used to understand how much a pointer
 * has moved relative to an element, even if that element is moving along with the changes to the
 * pointer.  For example, if a pointer touches a 1x1 pixel box in the middle, [current] will
 * report a position of (0, 0) when dispatched to it.  If the next event moves x position 5
 * pixels, [current] will report (5, 0) and [previous] will report (0, 0).  If the box moves all 5
 * pixels, and the next event represents the pointer moving along the x axis for 5 more pixels,
 * [current] will again report (5, 0) and [previous] will report (0, 0).
 *
 * @param id The unique id of the pointer associated with this [PointerInputChange].
 * @param current The [PointerInputData] that represents the current state of this pointer.
 * @param previous The [PointerInputData] that represents the previous state of this pointer.
 * @param consumed Which aspects of this change have been consumed.
 */
@Immutable
data class PointerInputChange(
    val id: PointerId,
    val current: PointerInputData,
    val previous: PointerInputData,
    val consumed: ConsumedData
)

/**
 * An ID for a given pointer.
 *
 * @param value The actual value of the id.
 */
inline class PointerId(val value: Long)

/**
 * Data associated with a pointer.
 *
 * @param uptime The time associated with this particular [PointerInputData]
 * @param position The position of the pointer at [uptime] relative to element that
 * the owning [PointerInputChange] is being dispatched to.
 * @param down True if the at [uptime] the pointer was contacting the screen.
 */
@Immutable
data class PointerInputData(
    @Stable
    val uptime: Uptime? = null,
    @Stable
    val position: Offset? = null,
    @Stable
    val down: Boolean = false
)

/**
 * Describes what aspects of, and how much of, a change has been consumed.
 *
 * @param positionChange The amount of change to the position that has been consumed.
 * @param downChange True if a change to down or up has been consumed.
 */
data class ConsumedData(
    var positionChange: Offset = Offset.Companion.Zero,
    var downChange: Boolean = false
)

/**
 * The enumeration of passes where [PointerInputChange] traverses up and down the UI tree.
 *
 * PointerInputChanges traverse throw the hierarchy in the following passes:
 *
 * 1. [Initial]: Down the tree from ancestor to descendant.
 * 2. [Main]: Up the tree from descendant to ancestor.
 * 3. [Final]: Down the tree from ancestor to descendant.
 *
 * These passes serve the following purposes:
 *
 * 1. Initial: Allows ancestors to consume aspects of [PointerInputChange] before descendants.
 * This is where, for example, a scroller may block buttons from getting tapped by other fingers
 * once scrolling has started.
 * 2. Main: The primary pass where gesture filters should react to and consume aspects of
 * [PointerInputChange]s. This is the primary path where descendants will interact with
 * [PointerInputChange]s before parents. This allows for buttons to respond to a tap before a
 * container of the bottom to respond to a tap.
 * 3. Final: This pass is where children can learn what aspects of [PointerInputChange]s were
 * consumed by parents during the [Main] pass. For example, this is how a button determines that
 * it should no longer respond to fingers lifting off of it because a parent scroller has
 * consumed movement in a [PointerInputChange].
 */
enum class PointerEventPass {
    Initial, Main, Final
}

/**
 * A function used to react to and modify [PointerInputChange]s.
 */
typealias PointerInputHandler =
            (PointerEvent, PointerEventPass, IntSize) -> List<PointerInputChange>

/**
 * The base type for all custom events.
 */
interface CustomEvent

/**
 * Defines the interface that is used to dispatch CustomEvents to pointer input nodes across the
 * compose tree.
 */
interface CustomEventDispatcher {

    /**
     * Dispatches the [event] to all other pointer input nodes that share associated [PointerId]s
     * with the pointer input node doing the dispatching.
     *
     * @param event The [CustomEvent] to dispatch.
     */
    // TODO(shepshapard): Come back and consider any issues with: This effectively allows
    //  individual pointer input nodes to gain a reference back to the internal HitPathTracker.
    //  But I think that is ok since pointer input nodes should  never be able to live for longer
    //  than the HitPathTracker that would be responsible for tracking them.
    fun dispatchCustomEvent(event: CustomEvent)

    /**
     * Arranges to retain the hit paths associated with the provided [pointerIds] such that if
     * they are requested to be removed for any reason, they are retained.
     *
     * For example, this is useful when a pointer input filter wants to be able to send future
     * custom messages to a another after the pointer has actually be released from the screen
     * (such as in the case where a Double Tap gesture detector may want to delay a Single Tap
     * gesture detector from firing but later may allow it to do so even after the pointer
     * associated with the Single Tap Gesture detector no longer exists.
     */
    fun retainHitPaths(pointerIds: Set<PointerId>)

    /**
     * Arranges to release any hit paths associated with the provided [pointerIds] such that if
     * they will be requested to be removed in the future, they will be removed upon request.
     *
     * If they were already requested to be removed while they were retained, they will be
     * removed immediately upon release.
     */
    fun releaseHitPaths(pointerIds: Set<PointerId>)
}

/**
 * True if this [PointerInputChange] represents a pointer coming in contact with the screen and
 * that change has not been consumed.
 */
fun PointerInputChange.changedToDown() = !consumed.downChange && !previous.down && current.down

/**
 * True if this [PointerInputChange] represents a pointer coming in contact with the screen, whether
 * or not that change has been consumed.
 */
fun PointerInputChange.changedToDownIgnoreConsumed() = !previous.down && current.down

/**
 * True if this [PointerInputChange] represents a pointer breaking contact with the screen and
 * that change has not been consumed.
 */
fun PointerInputChange.changedToUp() = !consumed.downChange && previous.down && !current.down

/**
 * True if this [PointerInputChange] represents a pointer breaking contact with the screen, whether
 * or not that change has been consumed.
 */
fun PointerInputChange.changedToUpIgnoreConsumed() = previous.down && !current.down

/**
 * True if this [PointerInputChange] represents a pointer moving on the screen and some of that
 * movement has not been consumed.
 */
fun PointerInputChange.positionChanged() =
    this.positionChangeInternal(false) != Offset.Companion.Zero

/**
 * True if this [PointerInputChange] represents a pointer moving on the screen ignoring how much
 * of that movement may have been consumed.
 */
fun PointerInputChange.positionChangedIgnoreConsumed() =
    this.positionChangeInternal(true) != Offset.Companion.Zero

/**
 * The distance that the pointer has moved on the screen minus any distance that has been consumed.
 */
fun PointerInputChange.positionChange() = this.positionChangeInternal(false)

/**
 * The distance that the pointer has moved on the screen, ignoring any distance that may have been
 * consumed.
 */
fun PointerInputChange.positionChangeIgnoreConsumed() = this.positionChangeInternal(true)
private fun PointerInputChange.positionChangeInternal(ignoreConsumed: Boolean = false): Offset {
    val previousPosition = previous.position
    val currentPosition = current.position

    val offset =
        if (previousPosition == null || currentPosition == null) {
            Offset(0.0f, 0.0f)
        } else {
            currentPosition - previousPosition
        }

    return if (!ignoreConsumed) {
        offset - consumed.positionChange
    } else {
        offset
    }
}

/**
 * True if any of this [PointerInputChange]'s movement has been consumed.
 */
fun PointerInputChange.anyPositionChangeConsumed() =
    consumed.positionChange.x != 0f || consumed.positionChange.y != 0f

/**
 * True if any aspect of this [PointerInputChange] has been consumed.
 */
fun PointerInputChange.anyChangeConsumed() = anyPositionChangeConsumed() || consumed.downChange

/**
 * Consume the up or down change of this [PointerInputChange] if there is an up or down change to
 * consume.
 */
fun PointerInputChange.consumeDownChange() {
    if (current.down != previous.down) {
        consumed.downChange = true
    }
}

/**
 * Consumes some portion of the position change of this [PointerInputChange].
 *
 * @param consumedDx The amount of position change on the x axis to consume.
 * @param consumedDy The amount of position change on the y axis to consume.
 */
fun PointerInputChange.consumePositionChange(
    consumedDx: Float,
    consumedDy: Float
) {
    // TODO(shepshapard): Handle case where consumption would make the consumption total to be
    //  less than the total change.
    consumed.positionChange += Offset(consumedDx, consumedDy)
}

/**
 * Consumes all changes associated with the [PointerInputChange]
 */
fun PointerInputChange.consumeAllChanges() {
    val remainingPositionChange = this.positionChange()
    this.consumeDownChange()
    this.consumePositionChange(remainingPositionChange.x, remainingPositionChange.y)
}