1 /*
2 * Copyright 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.compose.ui.node
18
19 import androidx.compose.ui.internal.requirePrecondition
20 import androidx.compose.ui.node.DpTouchBoundsExpansion.Companion.Absolute
21 import androidx.compose.ui.node.TouchBoundsExpansion.Companion.Absolute
22 import androidx.compose.ui.unit.Density
23 import androidx.compose.ui.unit.Dp
24 import androidx.compose.ui.unit.LayoutDirection
25 import androidx.compose.ui.unit.dp
26 import kotlin.jvm.JvmInline
27
28 /**
29 * Describes the expansion of a [PointerInputModifierNode]'s touch bounds along each edges. See
30 * [TouchBoundsExpansion] factories and [Absolute] for convenient ways to build
31 * [TouchBoundsExpansion].
32 *
33 * @see PointerInputModifierNode.touchBoundsExpansion
34 */
35 @JvmInline
36 value class TouchBoundsExpansion internal constructor(private val packedValue: Long) {
37 companion object {
38 /**
39 * Creates a [TouchBoundsExpansion] that's unaware of [LayoutDirection]. The `left`, `top`,
40 * `right` and `bottom` represent the amount of pixels that the touch bounds is expanded
41 * along the corresponding edge. Each value must be in the range of 0 to 32767 (inclusive).
42 */
Absolutenull43 fun Absolute(
44 left: Int = 0,
45 top: Int = 0,
46 right: Int = 0,
47 bottom: Int = 0
48 ): TouchBoundsExpansion {
49 requirePrecondition(left in 0..MAX_VALUE) {
50 "Start must be in the range of 0 .. $MAX_VALUE"
51 }
52 requirePrecondition(top in 0..MAX_VALUE) {
53 "Top must be in the range of 0 .. $MAX_VALUE"
54 }
55 requirePrecondition(right in 0..MAX_VALUE) {
56 "End must be in the range of 0 .. $MAX_VALUE"
57 }
58 requirePrecondition(bottom in 0..MAX_VALUE) {
59 "Bottom must be in the range of 0 .. $MAX_VALUE"
60 }
61 return TouchBoundsExpansion(pack(left, top, right, bottom, false))
62 }
63
64 /** Constant that represents no touch bounds expansion. */
65 val None = TouchBoundsExpansion(0)
66
packnull67 internal fun pack(
68 start: Int,
69 top: Int,
70 end: Int,
71 bottom: Int,
72 isLayoutDirectionAware: Boolean
73 ): Long {
74 return trimAndShift(start, 0) or
75 trimAndShift(top, 1) or
76 trimAndShift(end, 2) or
77 trimAndShift(bottom, 3) or
78 if (isLayoutDirectionAware) IS_LAYOUT_DIRECTION_AWARE else 0L
79 }
80
81 private const val MASK = 0x7FFF
82
83 private const val SHIFT = 15
84
85 internal const val MAX_VALUE = MASK
86
87 private const val IS_LAYOUT_DIRECTION_AWARE = 1L shl 63
88
89 // We stored all
unpacknull90 private fun unpack(packedValue: Long, position: Int): Int =
91 (packedValue shr (position * SHIFT)).toInt() and MASK
92
93 private fun trimAndShift(int: Int, position: Int): Long =
94 (int and MASK).toLong() shl (position * SHIFT)
95 }
96
97 /**
98 * The amount of pixels the touch bounds should be expanded along the start edge. When
99 * [isLayoutDirectionAware] is `true`, it's applied to the left edge when [LayoutDirection] is
100 * [LayoutDirection.Ltr] and vice versa. When [isLayoutDirectionAware] is `false`, it's always
101 * applied to the left edge.
102 */
103 val start: Int
104 get() = unpack(packedValue, 0)
105
106 /** The amount of pixels the touch bounds should be expanded along the top edge. */
107 val top: Int
108 get() = unpack(packedValue, 1)
109
110 /**
111 * The amount of pixels the touch bounds should be expanded along the end edge. When
112 * [isLayoutDirectionAware] is `true`, it's applied to the left edge when [LayoutDirection] is
113 * [LayoutDirection.Ltr] and vice versa. When [isLayoutDirectionAware] is `false`, it's always
114 * applied to the left edge.
115 */
116 val end: Int
117 get() = unpack(packedValue, 2)
118
119 /** The amount of pixels the touch bounds should be expanded along the bottom edge. */
120 val bottom: Int
121 get() = unpack(packedValue, 3)
122
123 /**
124 * Whether this [TouchBoundsExpansion] is aware of [LayoutDirection] or not. See [start] and
125 * [end] for more details.
126 */
127 val isLayoutDirectionAware: Boolean
128 get() = (packedValue and IS_LAYOUT_DIRECTION_AWARE) != 0L
129
130 /** Returns the amount of pixels the touch bounds is expanded towards left. */
131 internal fun computeLeft(layoutDirection: LayoutDirection): Int {
132 return if (!isLayoutDirectionAware || layoutDirection == LayoutDirection.Ltr) {
133 start
134 } else {
135 end
136 }
137 }
138
139 /** Returns the amount of pixels the touch bounds is expanded towards right. */
computeRightnull140 internal fun computeRight(layoutDirection: LayoutDirection): Int {
141 return if (!isLayoutDirectionAware || layoutDirection == LayoutDirection.Ltr) {
142 end
143 } else {
144 start
145 }
146 }
147 }
148
149 /**
150 * Describes the expansion of a [PointerInputModifierNode]'s touch bounds along each edges using
151 * [Dp] for units. See [DpTouchBoundsExpansion] factories and [Absolute] for convenient ways to
152 * build [DpTouchBoundsExpansion].
153 *
154 * @see PointerInputModifierNode.touchBoundsExpansion
155 */
156 @Suppress("DataClassDefinition")
157 data class DpTouchBoundsExpansion(
158 val start: Dp,
159 val top: Dp,
160 val end: Dp,
161 val bottom: Dp,
162 val isLayoutDirectionAware: Boolean
163 ) {
164 init {
<lambda>null165 requirePrecondition(start.value >= 0) { "Left must be non-negative" }
<lambda>null166 requirePrecondition(top.value >= 0) { "Top must be non-negative" }
<lambda>null167 requirePrecondition(end.value >= 0) { "Right must be non-negative" }
<lambda>null168 requirePrecondition(bottom.value >= 0) { "Bottom must be non-negative" }
169 }
170
roundToTouchBoundsExpansionnull171 fun roundToTouchBoundsExpansion(density: Density) =
172 with(density) {
173 TouchBoundsExpansion(
174 packedValue =
175 TouchBoundsExpansion.pack(
176 start.roundToPx(),
177 top.roundToPx(),
178 end.roundToPx(),
179 bottom.roundToPx(),
180 isLayoutDirectionAware
181 )
182 )
183 }
184
185 companion object {
186 /**
187 * Creates a [DpTouchBoundsExpansion] that's unaware of [LayoutDirection]. The `left`,
188 * `top`, `right` and `bottom` represent the distance that the touch bounds is expanded
189 * along the corresponding edge.
190 */
Absolutenull191 fun Absolute(
192 left: Dp = 0.dp,
193 top: Dp = 0.dp,
194 right: Dp = 0.dp,
195 bottom: Dp = 0.dp
196 ): DpTouchBoundsExpansion {
197 return DpTouchBoundsExpansion(left, top, right, bottom, false)
198 }
199 }
200 }
201
202 /**
203 * Creates a [TouchBoundsExpansion] that's aware of [LayoutDirection]. See
204 * [TouchBoundsExpansion.start] and [TouchBoundsExpansion.end] for more details about
205 * [LayoutDirection].
206 *
207 * The `start`, `top`, `end` and `bottom` represent the amount of pixels that the touch bounds is
208 * expanded along the corresponding edge. Each value must be in the range of 0 to 32767 (inclusive).
209 */
TouchBoundsExpansionnull210 fun TouchBoundsExpansion(
211 start: Int = 0,
212 top: Int = 0,
213 end: Int = 0,
214 bottom: Int = 0
215 ): TouchBoundsExpansion {
216 requirePrecondition(start in 0..TouchBoundsExpansion.MAX_VALUE) {
217 "Start must be in the range of 0 .. ${TouchBoundsExpansion.MAX_VALUE}"
218 }
219 requirePrecondition(top in 0..TouchBoundsExpansion.MAX_VALUE) {
220 "Top must be in the range of 0 .. ${TouchBoundsExpansion.MAX_VALUE}"
221 }
222 requirePrecondition(end in 0..TouchBoundsExpansion.MAX_VALUE) {
223 "End must be in the range of 0 .. ${TouchBoundsExpansion.MAX_VALUE}"
224 }
225 requirePrecondition(bottom in 0..TouchBoundsExpansion.MAX_VALUE) {
226 "Bottom must be in the range of 0 .. ${TouchBoundsExpansion.MAX_VALUE}"
227 }
228 return TouchBoundsExpansion(
229 packedValue = TouchBoundsExpansion.pack(start, top, end, bottom, true)
230 )
231 }
232
233 /**
234 * Creates a [DpTouchBoundsExpansion] that's aware of [LayoutDirection]. See
235 * [DpTouchBoundsExpansion.start] and [DpTouchBoundsExpansion.end] for more details about
236 * [LayoutDirection].
237 *
238 * The `start`, `top`, `end` and `bottom` represent the distance that the touch bounds is expanded
239 * along the corresponding edge.
240 */
DpTouchBoundsExpansionnull241 fun DpTouchBoundsExpansion(
242 start: Dp = 0.dp,
243 top: Dp = 0.dp,
244 end: Dp = 0.dp,
245 bottom: Dp = 0.dp
246 ): DpTouchBoundsExpansion {
247 return DpTouchBoundsExpansion(start, top, end, bottom, true)
248 }
249