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