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("INLINE_CLASS_DEPRECATED", "EXPERIMENTAL_FEATURE_WARNING")

package androidx.compose.ui.input.pointer

import androidx.compose.runtime.Immutable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
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.unit.IntSize

/**
 * 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
    )

    /**
     * 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()

    internal var layoutCoordinates: LayoutCoordinates? = null

    /**
     * The layout size assigned to this [PointerInputFilter].
     */
    val size: IntSize
        get() = layoutCoordinates?.size ?: IntSize.Zero

    internal var isAttached: Boolean = false

    /**
     * Intercept pointer input that children receive even if the pointer is out of bounds.
     *
     * If `true`, and a child has been moved out of this layout and receives an event, this
     * will receive that event. If `false`, a child receiving pointer input outside of the
     * bounds of this layout will not trigger any events in this.
     */
    open val interceptOutOfBoundsChildEvents: Boolean
        get() = false

    /**
     * If `false`, then this [PointerInputFilter] will not allow siblings under it to respond
     * to events. If `true`, this will have the first chance to respond and the next sibling
     * under will then get a chance to respond as well. This trigger acts as at the Layout
     * level, so if any [PointerInputFilter]s on a Layout has [shareWithSiblings] set to `true`
     * then the Layout will share with siblings.
     */
    @Suppress("EXPERIMENTAL_ANNOTATION_ON_WRONG_TARGET")
    @ExperimentalComposeUiApi
    @get:ExperimentalComposeUiApi
    open val shareWithSiblings: Boolean
        get() = false
}

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

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

    /**
     * The state of buttons (e.g. mouse or stylus buttons) during this event.
     */
    val buttons: PointerButtons

    /**
     * The state of modifier keys during this event.
     */
    val keyboardModifiers: PointerKeyboardModifiers

    /**
     * The primary reason the [PointerEvent] was sent.
     */
    var type: PointerEventType
        internal set
}

// TODO mark internal once https://youtrack.jetbrains.com/issue/KT-36695 is fixed
/* internal */ expect class NativePointerButtons

/**
 * Contains the state of pointer buttons (e.g. mouse and stylus buttons).
 */
inline class PointerButtons(internal val packedValue: NativePointerButtons)

/**
 * `true` when the primary button (left mouse button) is pressed or `false` when
 * it isn't pressed.
 */
expect val PointerButtons.isPrimaryPressed: Boolean

/**
 * `true` when the secondary button (right mouse button) is pressed or `false` when
 * it isn't pressed.
 */
expect val PointerButtons.isSecondaryPressed: Boolean

/**
 * `true` when the tertiary button (middle mouse button) is pressed or `false` when
 * it isn't pressed.
 */
expect val PointerButtons.isTertiaryPressed: Boolean

/**
 * `true` when the back button (mouse back button) is pressed or `false` when it isn't pressed or
 * there is no mouse button assigned to "back."
 */
expect val PointerButtons.isBackPressed: Boolean

/**
 * `true` when the forward button (mouse forward button) is pressed or `false` when it isn't pressed
 * or there is no button assigned to "forward."
 */
expect val PointerButtons.isForwardPressed: Boolean

/**
 * Returns `true` when the button at [buttonIndex] is pressed and `false` when it isn't pressed.
 * This method can handle buttons that haven't been assigned a designated purpose like
 * [isPrimaryPressed] and [isSecondaryPressed].
 */
expect fun PointerButtons.isPressed(buttonIndex: Int): Boolean

/**
 * Returns `true` if any button is pressed or `false` if all buttons are released.
 */
expect val PointerButtons.areAnyPressed: Boolean

/**
 * Returns the index of first button pressed as used in [isPressed] or `-1` if no button is pressed.
 */
expect fun PointerButtons.indexOfFirstPressed(): Int

/**
 * Returns the index of last button pressed as used in [isPressed] or `-1` if no button is pressed.
 */
expect fun PointerButtons.indexOfLastPressed(): Int

// TODO mark internal once https://youtrack.jetbrains.com/issue/KT-36695 is fixed
/* internal */ expect class NativePointerKeyboardModifiers

/**
 * Contains the state of modifier keys, such as Shift, Control, and Alt, as well as the state
 * of the lock keys, such as Caps Lock and Num Lock.
 */
inline class PointerKeyboardModifiers(internal val packedValue: NativePointerKeyboardModifiers)

/**
 * `true` when the Control key is pressed.
 */
expect val PointerKeyboardModifiers.isCtrlPressed: Boolean

/**
 * `true` when the Meta key is pressed. This is commonly associated with the Windows or Command
 * key on some keyboards.
 */
expect val PointerKeyboardModifiers.isMetaPressed: Boolean

/**
 * `true` when the Alt key is pressed. This is commonly associated with the Option key on some
 * keyboards.
 */
expect val PointerKeyboardModifiers.isAltPressed: Boolean

/**
 * `true` when the AltGraph key is pressed.
 */
expect val PointerKeyboardModifiers.isAltGraphPressed: Boolean

/**
 * `true` when the Sym key is pressed.
 */
expect val PointerKeyboardModifiers.isSymPressed: Boolean

/**
 * `true` when the Shift key is pressed.
 */
expect val PointerKeyboardModifiers.isShiftPressed: Boolean

/**
 * `true` when the Function key is pressed.
 */
expect val PointerKeyboardModifiers.isFunctionPressed: Boolean

/**
 * `true` when the keyboard's Caps Lock is on.
 */
expect val PointerKeyboardModifiers.isCapsLockOn: Boolean

/**
 * `true` when the keyboard's Scroll Lock is on.
 */
expect val PointerKeyboardModifiers.isScrollLockOn: Boolean

/**
 * `true` when the keyboard's Num Lock is on.
 */
expect val PointerKeyboardModifiers.isNumLockOn: Boolean

/**
 * The device type that produces a [PointerInputChange], such as a mouse or stylus.
 */
inline class PointerType private constructor(private val value: Int) {

    override fun toString(): String = when (value) {
        1 -> "Touch"
        2 -> "Mouse"
        3 -> "Stylus"
        4 -> "Eraser"
        else -> "Unknown"
    }

    companion object {
        /**
         * An unknown device type or the device type isn't relevant.
         */
        val Unknown = PointerType(0)

        /**
         * Touch (finger) input.
         */
        val Touch = PointerType(1)

        /**
         * A mouse pointer.
         */
        val Mouse = PointerType(2)

        /**
         * A stylus.
         */
        val Stylus = PointerType(3)

        /**
         * An eraser or an inverted stylus.
         */
        val Eraser = PointerType(4)
    }
}

/**
 * Indicates the primary reason that the [PointerEvent] was sent.
 */
inline class PointerEventType private constructor(internal val value: Int) {
    companion object {
        /**
         * An unknown reason for the event.
         */
        val Unknown = PointerEventType(0)

        /**
         * A button on the device was pressed or a new pointer was detected.
         */
        val Press = PointerEventType(1)

        /**
         * A button on the device was released or a pointer was raised.
         */
        val Release = PointerEventType(2)

        /**
         * The cursor or one or more touch pointers was moved.
         */
        val Move = PointerEventType(3)

        /**
         * The cursor has entered the input region. This will only be sent after the cursor is
         * hovering when in the input region.
         *
         * For example, the user's cursor is outside the input region and presses the button
         * prior to entering the input region. The [Enter] event will be sent when the button
         * is released inside the input region.
         */
        val Enter = PointerEventType(4)

        /**
         * A cursor device or elevated stylus exited the input region. This will only follow
         * an [Enter] event, so if a cursor with the button pressed enters and exits region,
         * neither [Enter] nor [Exit] will be sent for the input region. However, if a cursor
         * enters the input region, then a button is pressed, then the cursor exits and reenters,
         * [Enter], [Exit], and [Enter] will be received.
         */
        val Exit = PointerEventType(5)

        /**
         * A scroll event was sent. This can happen, for example, due to a mouse scroll wheel.
         * This event indicates that the [PointerInputChange.scrollDelta]'s [Offset] is non-zero.
         */
        @Suppress("EXPERIMENTAL_ANNOTATION_ON_WRONG_TARGET")
        @ExperimentalComposeUiApi
        @get:ExperimentalComposeUiApi
        val Scroll = PointerEventType(6)
    }

    @OptIn(ExperimentalComposeUiApi::class)
    override fun toString(): String = when (this) {
        Press -> "Press"
        Release -> "Release"
        Move -> "Move"
        Enter -> "Enter"
        Exit -> "Exit"
        Scroll -> "Scroll"
        else -> "Unknown"
    }
}

/**
 * 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 [position] represents the position of the pointer relative to the element that
 * this [PointerInputChange] is being dispatched to.
 *
 * The [previousPosition] represents the position of the pointer offset to the current
 * position of the pointer relative to the screen.
 *
 * This means that [position] and [previousPosition] 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,
 * [position] will report a position of (0, 0) when dispatched to it.  If the next event
 * moves x position 5 pixels, [position] will report (5, 0) and [previousPosition] 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, [position] will again report (5, 0) and
 * [previousPosition] will report (0, 0).
 *
 * @param id The unique id of the pointer associated with this [PointerInputChange].
 * @param uptimeMillis The time of the current pointer event, in milliseconds. The start (`0`) time
 * is platform-dependent
 * @param position The [Offset] of the current pointer event, relative to the containing
 * element
 * @param pressed `true` if the pointer event is considered "pressed." For example, finger
 * touching the screen or a mouse button is pressed [pressed] would be `true`.
 * @param previousUptimeMillis The [uptimeMillis] of the previous pointer event
 * @param previousPosition The [Offset] of the previous pointer event, offset to the
 * [position] and relative to the containing element.
 * @param previousPressed `true` if the pointer event was considered "pressed." For example , if
 * a finger was touching the screen or a mouse button was pressed, [previousPressed] would be
 * `true`.
 * @param consumed Which aspects of this change have been consumed.
 * @param type The device type that produced the event, such as [mouse][PointerType.Mouse],
 * or [touch][PointerType.Touch].git
 */
@Immutable
@OptIn(ExperimentalComposeUiApi::class)
class PointerInputChange(
    val id: PointerId,
    val uptimeMillis: Long,
    val position: Offset,
    val pressed: Boolean,
    val previousUptimeMillis: Long,
    val previousPosition: Offset,
    val previousPressed: Boolean,
    val consumed: ConsumedData,
    val type: PointerType = PointerType.Touch
) {
    /**
     * Optional high-frequency pointer moves in between the last two dispatched events.
     * Can be used for extra accuracy when touchscreen rate exceeds framerate.
     */
    // With these experimental annotations, the API can be either cleanly removed or
    // stabilized. It doesn't appear in current.txt; and in experimental_current.txt,
    // it has the same effect as a primary constructor val.
    @Suppress("EXPERIMENTAL_ANNOTATION_ON_WRONG_TARGET")
    @ExperimentalComposeUiApi
    @get:ExperimentalComposeUiApi
    val historical: List<HistoricalChange>
        get() = _historical ?: listOf()
    private var _historical: List<HistoricalChange>? = null

    /**
     * The amount of scroll wheel movement in the horizontal and vertical directions.
     */
    @Suppress("EXPERIMENTAL_ANNOTATION_ON_WRONG_TARGET")
    @ExperimentalComposeUiApi
    @get:ExperimentalComposeUiApi
    val scrollDelta: Offset
        get() = _scrollDelta
    private var _scrollDelta: Offset = Offset.Zero

    /**
     * Indicates whether the change was consumed or not. Note that the change must be consumed in
     * full as there's no partial consumption system provided.
     */
    @Suppress("EXPERIMENTAL_ANNOTATION_ON_WRONG_TARGET")
    @ExperimentalComposeUiApi
    @get:ExperimentalComposeUiApi
    val isConsumed: Boolean
        get() = consumed.downChange || consumed.positionChange

    /**
     * Consume change event, claiming all the corresponding change info to the caller. This is
     * usually needed when, button, when being clicked, consumed the "up" event so no other parents
     * of this button could consume this "up" again.
     *
     * "Consumption" is just an indication of the claim and each pointer input handler
     * implementation must manually check this flag to respect it.
     */
    @ExperimentalComposeUiApi
    fun consume() {
        consumed.downChange = true
        consumed.positionChange = true
    }

    internal constructor(
        id: PointerId,
        uptimeMillis: Long,
        position: Offset,
        pressed: Boolean,
        previousUptimeMillis: Long,
        previousPosition: Offset,
        previousPressed: Boolean,
        consumed: ConsumedData,
        type: PointerType,
        historical: List<HistoricalChange>,
        scrollDelta: Offset,
    ) : this(
        id,
        uptimeMillis,
        position,
        pressed,
        previousUptimeMillis,
        previousPosition,
        previousPressed,
        consumed,
        type
    ) {
        _historical = historical
        _scrollDelta = scrollDelta
    }

    fun copy(
        id: PointerId = this.id,
        currentTime: Long = this.uptimeMillis,
        currentPosition: Offset = this.position,
        currentPressed: Boolean = this.pressed,
        previousTime: Long = this.previousUptimeMillis,
        previousPosition: Offset = this.previousPosition,
        previousPressed: Boolean = this.previousPressed,
        consumed: ConsumedData = this.consumed,
        type: PointerType = this.type
    ): PointerInputChange = PointerInputChange(
        id,
        currentTime,
        currentPosition,
        currentPressed,
        previousTime,
        previousPosition,
        previousPressed,
        consumed,
        type,
        this.historical,
        this.scrollDelta
    )

    @ExperimentalComposeUiApi
    fun copy(
        id: PointerId = this.id,
        currentTime: Long = this.uptimeMillis,
        currentPosition: Offset = this.position,
        currentPressed: Boolean = this.pressed,
        previousTime: Long = this.previousUptimeMillis,
        previousPosition: Offset = this.previousPosition,
        previousPressed: Boolean = this.previousPressed,
        consumed: ConsumedData = this.consumed,
        type: PointerType = this.type,
        historical: List<HistoricalChange>
    ): PointerInputChange = PointerInputChange(
        id,
        currentTime,
        currentPosition,
        currentPressed,
        previousTime,
        previousPosition,
        previousPressed,
        consumed,
        type,
        historical,
        this.scrollDelta
    )

    internal fun copy(
        id: PointerId = this.id,
        currentTime: Long = this.uptimeMillis,
        currentPosition: Offset = this.position,
        currentPressed: Boolean = this.pressed,
        previousTime: Long = this.previousUptimeMillis,
        previousPosition: Offset = this.previousPosition,
        previousPressed: Boolean = this.previousPressed,
        consumed: ConsumedData = this.consumed,
        type: PointerType = this.type,
        historical: List<HistoricalChange> = this.historical,
        scrollDelta: Offset
    ): PointerInputChange = PointerInputChange(
        id,
        currentTime,
        currentPosition,
        currentPressed,
        previousTime,
        previousPosition,
        previousPressed,
        consumed,
        type,
        historical,
        scrollDelta
    )

    override fun toString(): String {
        return "PointerInputChange(id=$id, " +
            "uptimeMillis=$uptimeMillis, " +
            "position=$position, " +
            "pressed=$pressed, " +
            "previousUptimeMillis=$previousUptimeMillis, " +
            "previousPosition=$previousPosition, " +
            "previousPressed=$previousPressed, " +
            "consumed=$consumed, " +
            "type=$type, " +
            "historical=$historical," +
            "scrollDelta=$scrollDelta)"
    }
}

/**
 * Data structure for "historical" pointer moves.
 *
 * Optional high-frequency pointer moves in between the last two dispatched events:
 * can be used for extra accuracy when touchscreen rate exceeds framerate.
 *
 * @param uptimeMillis The time of the historical pointer event, in milliseconds. In between
 * the current and previous pointer event times.
 * @param position The [Offset] of the historical pointer event, relative to the containing
 * element.
 */
@Immutable
@ExperimentalComposeUiApi
class HistoricalChange(
    val uptimeMillis: Long,
    val position: Offset
) {
    override fun toString(): String {
        return "HistoricalChange(uptimeMillis=$uptimeMillis, " +
            "position=$position)"
    }
}

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

/**
 * Describes what aspects of a change has been consumed.
 *
 * @param positionChange True if a position change in this event has been consumed.
 * @param downChange True if a change to down or up has been consumed.
 */
class ConsumedData(
    @Suppress("GetterSetterNames")
    @get:Suppress("GetterSetterNames")
    var positionChange: Boolean = false,

    @Suppress("GetterSetterNames")
    @get:Suppress("GetterSetterNames")
    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
}

/**
 * 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 && !previousPressed && pressed

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

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

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

/**
 * 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 the fact that it might have
 * been consumed.
 */
fun PointerInputChange.positionChangeIgnoreConsumed() = this.positionChangeInternal(true)

private fun PointerInputChange.positionChangeInternal(ignoreConsumed: Boolean = false): Offset {
    val previousPosition = previousPosition
    val currentPosition = position

    val offset = currentPosition - previousPosition

    return if (!ignoreConsumed && consumed.positionChange) Offset.Zero else offset
}

/**
 * True if this [PointerInputChange]'s movement has been consumed.
 */
fun PointerInputChange.positionChangeConsumed() = consumed.positionChange

/**
 * True if any aspect of this [PointerInputChange] has been consumed.
 */
fun PointerInputChange.anyChangeConsumed() = positionChangeConsumed() || 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 (pressed != previousPressed) {
        consumed.downChange = true
    }
}

/**
 * Consume position change if there is any
 */
fun PointerInputChange.consumePositionChange() {
    if (positionChange() != Offset.Zero) {
        consumed.positionChange = true
    }
}

/**
 * Consumes all changes associated with the [PointerInputChange]
 */
fun PointerInputChange.consumeAllChanges() {
    this.consumeDownChange()
    this.consumePositionChange()
}

/**
 * Returns `true` if the pointer has moved outside of the region of
 * `(0, 0, size.width, size.height)` or `false` if the current pointer is up or it is inside the
 * given bounds.
 */
@Deprecated(
    message = "Use isOutOfBounds() that supports minimum touch target",
    replaceWith = ReplaceWith("this.isOutOfBounds(size, extendedTouchPadding)")
)
fun PointerInputChange.isOutOfBounds(size: IntSize): Boolean {
    val position = position
    val x = position.x
    val y = position.y
    val width = size.width
    val height = size.height
    return x < 0f || x > width || y < 0f || y > height
}

/**
 * Returns `true` if the pointer has moved outside of the pointer region. For Touch
 * events, this is (-extendedTouchPadding.width, -extendedTouchPadding.height,
 * size.width + extendedTouchPadding.width, size.height + extendedTouchPadding.height) and
 * for other events, this is `(0, 0, size.width, size.height)`. Returns`false` if the
 * current pointer is up or it is inside the pointer region.
 */
fun PointerInputChange.isOutOfBounds(size: IntSize, extendedTouchPadding: Size): Boolean {
    if (type != PointerType.Touch) {
        @Suppress("DEPRECATION")
        return isOutOfBounds(size)
    }
    val position = position
    val x = position.x
    val y = position.y
    val minX = -extendedTouchPadding.width
    val maxX = size.width + extendedTouchPadding.width
    val minY = -extendedTouchPadding.height
    val maxY = size.height + extendedTouchPadding.height
    return x < minX || x > maxX || y < minY || y > maxY
}