Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* 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.test
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.isSpecified
import androidx.compose.ui.geometry.lerp
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.util.lerp
import kotlin.math.max
import kotlin.math.roundToInt
/**
* The time between the button pressed and button released event in a mouse click. Determined by
* empirical sampling.
*/
private const val SingleClickDelayMillis = 60L
/**
* The default duration of mouse gestures with configurable time (e.g. [animateTo]).
*/
private const val DefaultMouseGestureDurationMillis: Long = 300L
/**
* The receiver scope of the mouse input injection lambda from [performMouseInput].
*
* The functions in [MouseInjectionScope] can roughly be divided into two groups: full gestures
* and individual mouse events. The individual mouse events are: [press], [moveTo] and friends,
* [release], [cancel], [scroll] and [advanceEventTime]. Full gestures are all the other
* functions, like [MouseInjectionScope.click], [MouseInjectionScope.doubleClick],
* [MouseInjectionScope.animateTo], etc. These are built on top of the individual events and
* serve as a good example on how you can build your own full gesture functions.
*
* A mouse move event can be sent with [moveTo] and [moveBy]. The mouse position can be updated
* with [updatePointerTo] and [updatePointerBy], which will not send an event and only update the
* position internally. This can be useful if you want to send an event that is not a move event
* with a location other then the current location, but without sending a preceding move event.
* Use [press] and [release] to send button pressed and button released events. This will also
* send all other necessary events that keep the stream of mouse events consistent with actual
* mouse input, such as a hover exit event. A [cancel] event can be sent at any time when at
* least one button is pressed. Use [scroll] to send a mouse scroll event.
*
* The entire event injection state is shared between all `perform.*Input` methods, meaning you
* can continue an unfinished mouse gesture in a subsequent invocation of [performMouseInput] or
* [performMultiModalInput]. Note however that while the mouse's position is retained across
* invocation of `perform.*Input` methods, it is always manipulated in the current node's local
* coordinate system. That means that two subsequent invocations of [performMouseInput] on
* different nodes will report a different [currentPosition], even though it is actually the same
* position on the screen.
*
* All events generated by these methods are enqueued and sent together when the scope is
* [flushed][flush]. This can be done at any time during the lambda passed to [performMouseInput],
* but the scope is always flushed after the execution of that lambda so normally you don't need
* to flush it yourself.
*
* Example usage:
* ```
* onNodeWithTag("myWidget")
* .performMouseInput {
* click(center)
* }
*
* onNodeWithTag("myWidget")
* // Scroll down while the primary mouse button is down:
* .performMouseInput {
* down()
* repeat(6) {
* advanceEventTime()
* scroll(-1f)
* }
* advanceEventTime()
* up()
* }
* ```
*
* @see InjectionScope
*/
@Suppress("NotCloseable")
@ExperimentalTestApi
interface MouseInjectionScope : InjectionScope {
/**
* Returns the current position of the mouse. The position is returned in the local
* coordinate system of the node with which we're interacting. (0, 0) is the top left corner
* of the node. If none of the move or updatePointer methods have been used yet, the mouse's
* position will be (0, 0) in the Compose host's coordinate system, which will be
* `-[topLeft]` in the node's local coordinate system.
*/
val currentPosition: Offset
/**
* Sends a move event [delayMillis] after the last sent event on the associated node, with
* the position of the mouse updated to [position]. The [position] is in the node's local
* coordinate system, where (0, 0) is the top left corner of the node.
*
* If no mouse buttons are pressed, a hover event will be sent instead of a move event. If
* the mouse wasn't hovering yet, a hover enter event is sent as well.
*
* @param position The new position of the mouse, in the node's local coordinate system
* @param delayMillis The time between the last sent event and this event.
* [eventPeriodMillis] by default.
*/
fun moveTo(position: Offset, delayMillis: Long = eventPeriodMillis)
/**
* Sends a move event [delayMillis] after the last sent event on the associated node, with
* the position of the mouse moved by the given [delta].
*
* If no mouse buttons are pressed, a hover event will be sent instead of a move event. If
* the mouse wasn't hovering yet, a hover enter event is sent as well.
*
* @param delta The position for this move event, relative to the current position of the
* mouse. For example, `delta = Offset(10.px, -10.px) will add 10.px to the mouse's
* x-position, and subtract 10.px from the mouse's y-position.
* @param delayMillis The time between the last sent event and this event.
* [eventPeriodMillis] by default.
*/
fun moveBy(delta: Offset, delayMillis: Long = eventPeriodMillis) {
moveTo(currentPosition + delta, delayMillis)
}
/**
* Updates the position of the mouse to the given [position], but does not send a move or
* hover event. This can be useful to adjust the mouse position before sending for example a
* [press] event. The [position] is in the node's local coordinate system, where (0.px, 0.px)
* is the top left corner of the node.
*
* @param position The new position of the mouse, in the node's local coordinate system
*/
fun updatePointerTo(position: Offset)
/**
* Updates the position of the mouse by the given [delta], but does not send a move or hover
* event. This can be useful to adjust the mouse position before sending for example a
* [press] event.
*
* @param delta The position for this move event, relative to the current position of the
* mouse. For example, `delta = Offset(10.px, -10.px) will add 10.px to the mouse's
* x-position, and subtract 10.px from the mouse's y-position.
*/
fun updatePointerBy(delta: Offset) {
updatePointerTo(currentPosition + delta)
}
/**
* Sends a down and button pressed event for the given [button] on the associated node. When
* no buttons were down yet, this will exit hovering mode before the button is pressed. All
* events will be sent at the current event time.
*
* Throws an [IllegalStateException] if the [button] is already pressed.
*
* @param button The mouse button that is pressed. By default the primary mouse button.
*/
fun press(button: MouseButton = MouseButton.Primary)
/**
* Sends a button released and up event for the given [button] on the associated node. If
* this was the last button to be released, the mouse will enter hovering mode and send an
* accompanying mouse move event after the button has been released. All events will be sent
* at the current event time.
*
* Throws an [IllegalStateException] if the [button] is not pressed.
*
* @param button The mouse button that is released. By default the primary mouse button.
*/
fun release(button: MouseButton = MouseButton.Primary)
/**
* Sends a cancel event [delayMillis] after the last sent event to cancel a stream of mouse
* events with pressed mouse buttons. All buttons will be released as a result. A mouse
* cancel event can only be sent when mouse buttons are pressed.
*
* @param delayMillis The time between the last sent event and this event.
* [eventPeriodMillis] by default.
*/
fun cancel(delayMillis: Long = eventPeriodMillis)
/**
* Sends a hover enter event at the given [position], [delayMillis] after the last sent event.
*
* Note that it is discouraged to manually send a hover enter event followed by a [moveTo]
* event. [moveTo] does this for you automatically, making sure the event times of the two
* events are in sync. Only use this method for special scenarios where the hover enter event
* is not sent as a side effect of another event.
*
* An [IllegalStateException] will be thrown when mouse buttons are down, or if the mouse is
* already hovering.
*
* The [position] is in the node's local coordinate system, where (0, 0) is the top left
* corner of the node.
*
* @param position The new position of the mouse, in the node's local coordinate system.
* [currentPosition] by default.
* @param delayMillis The time between the last sent event and this event.
* [eventPeriodMillis] by default.
*/
fun enter(position: Offset = currentPosition, delayMillis: Long = eventPeriodMillis)
/**
* Sends a hover exit event at the given [position], [delayMillis] after the last sent event.
*
* Note that it is discouraged to manually send a hover exit event followed by a [moveTo]
* that is outside the boundaries of the Compose root or [press]ing a button. These methods
* do this for you automatically, making sure the event times of the two events are in sync.
* Only use this method for special scenarios where the hover exit event is not sent as a
* side effect of another event.
*
* An [IllegalStateException] will be thrown if the mouse was not hovering.
*
* The [position] is in the node's local coordinate system, where (0, 0) is the top left
* corner of the node.
*
* @param position The new position of the mouse, in the node's local coordinate system
* [currentPosition] by default.
* @param delayMillis The time between the last sent event and this event.
* [eventPeriodMillis] by default.
*/
fun exit(position: Offset = currentPosition, delayMillis: Long = eventPeriodMillis)
/**
* Sends a scroll event with the given [delta] on the given [scrollWheel]. The event will be
* sent at the current event time. Negative [delta] values correspond to rotating the scroll
* wheel leftward or downward, positive values correspond to rotating the scroll wheel
* rightward or upward.
*
* @param delta The amount of scroll
* @param scrollWheel Which scroll wheel to rotate. Can be either [ScrollWheel.Vertical] (the
* default) or [ScrollWheel.Horizontal].
*/
fun scroll(delta: Float, scrollWheel: ScrollWheel = ScrollWheel.Vertical)
}
/**
* Click on [position], or on the current mouse position if [position] is
* [unspecified][Offset.Unspecified]. The [position] is in the node's local coordinate system,
* where (0, 0) is the top left corner of the node.
*
* @param position The position where to click, in the node's local coordinate system. If
* omitted, the [center] of the node will be used. If [unspecified][Offset.Unspecified], clicks
* on the current mouse position.
*/
@ExperimentalTestApi
fun MouseInjectionScope.click(position: Offset = center) {
if (position.isSpecified) {
updatePointerTo(position)
}
press(MouseButton.Primary)
advanceEventTime(SingleClickDelayMillis)
release(MouseButton.Primary)
}
/**
* Secondary-click on [position], or on the current mouse position if [position] is
* [unspecified][Offset.Unspecified]. While the secondary mouse button is not necessarily the
* right mouse button (e.g. on left-handed mice), this method is still called `rightClick` for
* it's widespread use. The [position] is in the node's local coordinate system, where (0, 0) is
* the top left corner of the node.
*
* @param position The position where to click, in the node's local coordinate system. If
* omitted, the [center] of the node will be used. If [unspecified][Offset.Unspecified], clicks
* on the current mouse position.
*/
@ExperimentalTestApi
fun MouseInjectionScope.rightClick(position: Offset = center) {
if (position.isSpecified) {
updatePointerTo(position)
}
press(MouseButton.Secondary)
advanceEventTime(SingleClickDelayMillis)
release(MouseButton.Secondary)
}
// The average of min and max is a safe default
private val ViewConfiguration.defaultDoubleTapDelayMillis: Long
get() = (doubleTapMinTimeMillis + doubleTapTimeoutMillis) / 2
/**
* Double-click on [position], or on the current mouse position if [position] is
* [unspecified][Offset.Unspecified]. The [position] is in the node's local coordinate system,
* where (0, 0) is the top left corner of the node.
*
* @param position The position where to click, in the node's local coordinate system. If
* omitted, the [center] of the node will be used. If [unspecified][Offset.Unspecified], clicks
* on the current mouse position.
*/
@ExperimentalTestApi
fun MouseInjectionScope.doubleClick(position: Offset = center) {
click(position)
advanceEventTime(viewConfiguration.defaultDoubleTapDelayMillis)
click(position)
}
/**
* Triple-click on [position], or on the current mouse position if [position] is
* [unspecified][Offset.Unspecified]. The [position] is in the node's local coordinate system,
* where (0, 0) is the top left corner of the node.
*
* @param position The position where to click, in the node's local coordinate system. If
* omitted, the [center] of the node will be used. If [unspecified][Offset.Unspecified], clicks
* on the current mouse position.
*/
@ExperimentalTestApi
fun MouseInjectionScope.tripleClick(position: Offset = center) {
click(position)
advanceEventTime(viewConfiguration.defaultDoubleTapDelayMillis)
click(position)
advanceEventTime(viewConfiguration.defaultDoubleTapDelayMillis)
click(position)
}
/**
* Long-click on [position], or on the current mouse position if [position] is
* [unspecified][Offset.Unspecified]. The [position] is in the node's local coordinate system,
* where (0, 0) is the top left corner of the node.
*
* @param position The position where to click, in the node's local coordinate system. If
* omitted, the [center] of the node will be used. If [unspecified][Offset.Unspecified], clicks
* on the current mouse position.
*/
@ExperimentalTestApi
fun MouseInjectionScope.longClick(position: Offset = center) {
if (position.isSpecified) {
updatePointerTo(position)
}
press(MouseButton.Primary)
advanceEventTime(viewConfiguration.longPressTimeoutMillis + 100L)
release(MouseButton.Primary)
}
/**
* Move the mouse from the [current position][MouseInjectionScope.currentPosition] to the given
* [position], sending a stream of move events to get an animated path of [durationMillis]
* milliseconds. [Move][moveTo] the mouse to the desired start position if you want to start from
* a different position. The [position] is in the node's local coordinate system, where (0, 0) is
* the top left corner of the node.
*
* @param position The position where to move the mouse to, in the node's local coordinate system
* @param durationMillis The duration of the gesture. By default 300 milliseconds.
*/
@ExperimentalTestApi
fun MouseInjectionScope.animateTo(
position: Offset,
durationMillis: Long = DefaultMouseGestureDurationMillis
) {
val durationFloat = durationMillis.toFloat()
val start = currentPosition
animateAlong(
curve = { lerp(start, position, it / durationFloat) },
durationMillis = durationMillis
)
}
/**
* Move the mouse from the [current position][MouseInjectionScope.currentPosition] by the given
* [delta], sending a stream of move events to get an animated path of [durationMillis]
* milliseconds.
*
* @param delta The position where to move the mouse to, relative to the current position of the
* mouse. For example, `delta = Offset(100.px, -100.px) will move the mouse 100 pixels to the
* right and 100 pixels upwards.
* @param durationMillis The duration of the gesture. By default 300 milliseconds.
*/
@ExperimentalTestApi
fun MouseInjectionScope.animateBy(
delta: Offset,
durationMillis: Long = DefaultMouseGestureDurationMillis
) {
animateTo(currentPosition + delta, durationMillis)
}
/**
* Move the mouse along the given [curve], sending a stream of move events to get an animated
* path of [durationMillis] milliseconds. The mouse will initially be moved to the start of the
* path, `curve(0)`, if it is not already there. The positions defined by the [curve] are in the
* node's local coordinate system, where (0, 0) is the top left corner of the node.
*
* @param curve The function that defines the position of the mouse over time for this gesture,
* in the node's local coordinate system.
* @param durationMillis The duration of the gesture. By default 300 milliseconds.
*/
@ExperimentalTestApi
fun MouseInjectionScope.animateAlong(
curve: (Long) -> Offset,
durationMillis: Long = DefaultMouseGestureDurationMillis
) {
require(durationMillis > 0) {
"Duration is 0"
}
val start = curve(0)
if (start != currentPosition) {
// Instantly move to the start position to maintain the total durationMillis
moveTo(curve(0), delayMillis = 0)
}
var step = 0
// How many steps will we take in durationMillis?
// At least 1, and a number that will bring as as close to eventPeriod as possible
val steps = max(1, (durationMillis / eventPeriodMillis.toFloat()).roundToInt())
var tPrev = 0L
while (step++ < steps) {
val progress = step / steps.toFloat()
val t = lerp(0, durationMillis, progress)
moveTo(curve(t), delayMillis = t - tPrev)
tPrev = t
}
}
/**
* Drag and drop something from [start] to [end] in [durationMillis] milliseconds. This gesture
* uses the primary mouse button to drag with, and does not reset any mouse buttons prior to
* starting the gesture. The mouse position is [updated][MouseInjectionScope.updatePointerTo] to
* the start position before starting the gesture. The positions defined by the [start] and [end]
* are in the node's local coordinate system, where (0, 0) is the top left corner of the node.
*
* @param start The position where to press the primary mouse button and initiate the drag, in
* the node's local coordinate system.
* @param end The position where to release the primary mouse button and end the drag, in the
* node's local coordinate system.
* @param durationMillis The duration of the gesture. By default 300 milliseconds.
*/
@ExperimentalTestApi
fun MouseInjectionScope.dragAndDrop(
start: Offset,
end: Offset,
durationMillis: Long = DefaultMouseGestureDurationMillis
) {
updatePointerTo(start)
press(MouseButton.Primary)
animateTo(end, durationMillis)
release(MouseButton.Primary)
}
/**
* Rotate the mouse's [scrollWheel] by the given [scrollAmount]. The total scroll delta is
* linearly smoothed out over a stream of scroll events with roughly the
* [InjectionScope.eventPeriodMillis] between each scroll event. Negative [scrollAmount] values
* correspond to rotating the scroll wheel leftward or downward, positive values correspond to
* rotating the scroll wheel rightward or upward.
*
* @param scrollAmount The total delta to scroll the [scrollWheel] by
* @param durationMillis The duration of the gesture. By default 300 milliseconds.
* @param scrollWheel Which scroll wheel will be rotated. By default [ScrollWheel.Vertical].
*/
@ExperimentalTestApi
fun MouseInjectionScope.smoothScroll(
scrollAmount: Float,
durationMillis: Long = DefaultMouseGestureDurationMillis,
scrollWheel: ScrollWheel = ScrollWheel.Vertical
) {
var step = 0
// How many steps will we take in durationMillis?
// At least 1, and a number that will bring as as close to eventPeriod as possible
val steps = max(1, (durationMillis / eventPeriodMillis.toFloat()).roundToInt())
var tPrev = 0L
var valuePrev = 0f
while (step++ < steps) {
val progress = step / steps.toFloat()
val t = lerp(0, durationMillis, progress)
val value = lerp(0f, scrollAmount, progress)
advanceEventTime(t - tPrev)
scroll(value - valuePrev, scrollWheel)
tPrev = t
valuePrev = value
}
}