1 /*
2  * Copyright (C) 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.ink.brush
18 
19 import androidx.ink.brush.color.Color
20 import androidx.ink.brush.color.colorspace.ColorSpaces
21 import androidx.ink.brush.color.toArgb
22 import androidx.ink.nativeloader.UsedByNative
23 import com.google.common.truth.Truth.assertThat
24 import kotlin.test.assertFailsWith
25 import org.junit.Test
26 import org.junit.runner.RunWith
27 import org.junit.runners.JUnit4
28 
29 @OptIn(ExperimentalInkCustomBrushApi::class)
30 @RunWith(JUnit4::class)
31 class BrushTest {
32     private val size = 10F
33     private val epsilon = 1F
34     private val color = Color(red = 230, green = 115, blue = 140, alpha = 255)
35     private val family = BrushFamily(clientBrushFamilyId = "/brush-family:inkpen:1")
36 
37     @Test
constructor_withValidArguments_returnsABrushnull38     fun constructor_withValidArguments_returnsABrush() {
39         val brush = Brush.createWithColorLong(family, color.value.toLong(), size, epsilon)
40         assertThat(brush).isNotNull()
41         assertThat(brush.family).isEqualTo(family)
42         assertThat(brush.colorLong).isEqualTo(color.value.toLong())
43         assertThat(brush.colorIntArgb).isEqualTo(color.toArgb())
44         assertThat(brush.colorLong).isEqualTo(color.value.toLong())
45         assertThat(brush.size).isEqualTo(size)
46         assertThat(brush.epsilon).isEqualTo(epsilon)
47     }
48 
49     @Test
50     @Suppress("Range") // Testing error cases.
constructor_withBadSize_willThrownull51     fun constructor_withBadSize_willThrow() {
52         assertFailsWith<IllegalArgumentException> {
53             Brush(family, color, -2F, epsilon) // non-positive size.
54         }
55 
56         assertFailsWith<IllegalArgumentException> {
57             Brush(family, color, Float.POSITIVE_INFINITY, epsilon) // non-finite size.
58         }
59 
60         assertFailsWith<IllegalArgumentException> {
61             Brush(family, color, Float.NaN, epsilon) // non-finite size.
62         }
63     }
64 
65     @Test
66     @Suppress("Range") // Testing error cases.
constructor_withBadEpsilon_willThrownull67     fun constructor_withBadEpsilon_willThrow() {
68         assertFailsWith<IllegalArgumentException> {
69             Brush(family, color, size, -2F) // non-positive epsilon.
70         }
71 
72         assertFailsWith<IllegalArgumentException> {
73             Brush(family, color, size, Float.POSITIVE_INFINITY) // non-finite epsilon.
74         }
75 
76         assertFailsWith<IllegalArgumentException> {
77             Brush(family, color, size, Float.NaN) // non-finite epsilon.
78         }
79     }
80 
81     @Test
colorAccessors_areAllEquivalentnull82     fun colorAccessors_areAllEquivalent() {
83         val color = Color(red = 230, green = 115, blue = 140, alpha = 196)
84         val brush = Brush.createWithColorLong(family, color.value.toLong(), size, epsilon)
85 
86         assertThat(brush.colorIntArgb).isEqualTo(color.toArgb())
87         assertThat(brush.colorLong).isEqualTo(color.value.toLong())
88     }
89 
90     @Test
withColorIntArgb_withLowAlpha_returnsBrushWithCorrectColornull91     fun withColorIntArgb_withLowAlpha_returnsBrushWithCorrectColor() {
92         val brush = Brush.createWithColorIntArgb(family, 0x12345678, size, epsilon)
93         assertThat(brush.colorIntArgb).isEqualTo(0x12345678)
94     }
95 
96     @Test
withColorIntArgb_withHighAlpha_returnsBrushWithCorrectColornull97     fun withColorIntArgb_withHighAlpha_returnsBrushWithCorrectColor() {
98         val brush = Brush.createWithColorIntArgb(family, 0xAA123456.toInt(), size, epsilon)
99         assertThat(brush.colorIntArgb).isEqualTo(0xAA123456.toInt())
100     }
101 
102     @Test
withColorLong_returnsBrushWithCorrectColornull103     fun withColorLong_returnsBrushWithCorrectColor() {
104         val colorLong = Color(0.9f, 0.45f, 0.55f, 0.15f, ColorSpaces.DisplayP3).value.toLong()
105         val brush = Brush.createWithColorLong(family, colorLong, size, epsilon)
106         assertThat(brush.colorLong).isEqualTo(colorLong)
107     }
108 
109     @Test
withColorLong_inUnsupportedColorSpace_returnsBrushWithConvertedColornull110     fun withColorLong_inUnsupportedColorSpace_returnsBrushWithConvertedColor() {
111         val colorLong = Color(0.9f, 0.45f, 0.55f, 0.15f, ColorSpaces.AdobeRgb).value.toLong()
112         val brush = Brush.createWithColorLong(family, colorLong, size, epsilon)
113 
114         val expectedColor = Color(colorLong.toULong()).convert(ColorSpaces.DisplayP3)
115         assertThat(brush.colorLong).isEqualTo(expectedColor.value.toLong())
116         assertThat(brush.colorIntArgb).isEqualTo(expectedColor.toArgb())
117     }
118 
119     @Test
equals_returnsTrueForIdenticalBrushesnull120     fun equals_returnsTrueForIdenticalBrushes() {
121         val brush = Brush(family, color, size, epsilon)
122         val otherBrush = Brush(family, color, size, epsilon)
123         assertThat(brush == brush).isTrue()
124         assertThat(brush == otherBrush).isTrue()
125         assertThat(otherBrush == brush).isTrue()
126     }
127 
128     @Test
hashCode_isEqualForIdenticalBrushesnull129     fun hashCode_isEqualForIdenticalBrushes() {
130         val brush = Brush(family, color, size, epsilon)
131         val otherBrush = Brush(family, color, size, epsilon)
132         assertThat(brush == brush).isTrue()
133         assertThat(brush == otherBrush).isTrue()
134         assertThat(otherBrush == brush).isTrue()
135     }
136 
137     @Test
equals_returnsFalseIfAnyFieldsDiffernull138     fun equals_returnsFalseIfAnyFieldsDiffer() {
139         val brush = Brush(family, color, size, epsilon)
140 
141         val differentFamilyBrush =
142             Brush(BrushFamily(clientBrushFamilyId = "/brush-family:pencil:1"), color, size, epsilon)
143         assertThat(brush == differentFamilyBrush).isFalse()
144         assertThat(differentFamilyBrush == brush).isFalse()
145         assertThat(brush != differentFamilyBrush).isTrue()
146         assertThat(differentFamilyBrush != brush).isTrue()
147 
148         val otherColor =
149             Color(red = 1F, green = 0F, blue = 0F, alpha = 1F, colorSpace = ColorSpaces.DisplayP3)
150                 .value
151                 .toLong()
152         val differentcolorBrush = Brush.createWithColorLong(family, otherColor, size, epsilon)
153         assertThat(brush == differentcolorBrush).isFalse()
154         assertThat(differentcolorBrush == brush).isFalse()
155         assertThat(brush != differentcolorBrush).isTrue()
156         assertThat(differentcolorBrush != brush).isTrue()
157 
158         val differentSizeBrush = Brush(family, color, 9.0f, epsilon)
159         assertThat(brush == differentSizeBrush).isFalse()
160         assertThat(differentSizeBrush == brush).isFalse()
161         assertThat(brush != differentSizeBrush).isTrue()
162         assertThat(differentSizeBrush != brush).isTrue()
163 
164         val differentEpsilonBrush = Brush(family, color, size, 1.1f)
165         assertThat(brush == differentEpsilonBrush).isFalse()
166         assertThat(differentEpsilonBrush == brush).isFalse()
167         assertThat(brush != differentEpsilonBrush).isTrue()
168         assertThat(differentEpsilonBrush != brush).isTrue()
169     }
170 
171     @Test
hashCode_differsIfAnyFieldsDiffernull172     fun hashCode_differsIfAnyFieldsDiffer() {
173         val brush = Brush(family, color, size, epsilon)
174 
175         val differentFamilyBrush =
176             Brush(BrushFamily(clientBrushFamilyId = "/brush-family:pencil:1"), color, size, epsilon)
177         assertThat(differentFamilyBrush.hashCode()).isNotEqualTo(brush.hashCode())
178 
179         val otherColor =
180             Color(red = 1F, green = 0F, blue = 0F, alpha = 1F, colorSpace = ColorSpaces.DisplayP3)
181                 .value
182                 .toLong()
183         val differentcolorBrush = Brush.createWithColorLong(family, otherColor, size, epsilon)
184         assertThat(differentcolorBrush.hashCode()).isNotEqualTo(brush.hashCode())
185 
186         val differentSizeBrush = Brush(family, color, 9.0f, epsilon)
187         assertThat(differentSizeBrush.hashCode()).isNotEqualTo(brush.hashCode())
188 
189         val differentEpsilonBrush = Brush(family, color, size, 1.1f)
190         assertThat(differentEpsilonBrush.hashCode()).isNotEqualTo(brush.hashCode())
191     }
192 
193     @Test
copy_returnsTheSameBrushnull194     fun copy_returnsTheSameBrush() {
195         val originalBrush = buildTestBrush()
196 
197         val newBrush = originalBrush.copy()
198 
199         // A pure copy returns `this`.
200         assertThat(newBrush).isSameInstanceAs(originalBrush)
201     }
202 
203     @Test
copy_withChangedBrushFamily_returnsCopyWithDifferentBrushFamilynull204     fun copy_withChangedBrushFamily_returnsCopyWithDifferentBrushFamily() {
205         val originalBrush = buildTestBrush()
206 
207         val newBrush = originalBrush.copy(family = BrushFamily())
208 
209         assertThat(newBrush).isNotEqualTo(originalBrush)
210         assertThat(newBrush.family).isNotEqualTo(originalBrush.family)
211 
212         // The new brush has the original color, size and epsilon.
213         assertThat(newBrush.colorLong).isEqualTo(originalBrush.colorLong)
214         assertThat(newBrush.size).isEqualTo(originalBrush.size)
215         assertThat(newBrush.epsilon).isEqualTo(originalBrush.epsilon)
216     }
217 
218     @Test
copyWithColorIntArgb_withLowAlpha_returnsCopyWithThatColornull219     fun copyWithColorIntArgb_withLowAlpha_returnsCopyWithThatColor() {
220         val originalBrush = buildTestBrush()
221 
222         val newBrush = originalBrush.copyWithColorIntArgb(colorIntArgb = 0x12345678)
223 
224         assertThat(newBrush).isNotEqualTo(originalBrush)
225         assertThat(newBrush.colorLong).isNotEqualTo(originalBrush.colorLong)
226         assertThat(newBrush.colorIntArgb).isEqualTo(0x12345678)
227 
228         // The new brush has the original family, size and epsilon.
229         assertThat(newBrush.family).isSameInstanceAs(originalBrush.family)
230         assertThat(newBrush.size).isEqualTo(originalBrush.size)
231         assertThat(newBrush.epsilon).isEqualTo(originalBrush.epsilon)
232     }
233 
234     @Test
copyWithColorIntArgb_withHighAlpha_returnsCopyWithThatColornull235     fun copyWithColorIntArgb_withHighAlpha_returnsCopyWithThatColor() {
236         val originalBrush = buildTestBrush()
237 
238         val newBrush = originalBrush.copyWithColorIntArgb(colorIntArgb = 0xAA123456.toInt())
239 
240         assertThat(newBrush).isNotEqualTo(originalBrush)
241         assertThat(newBrush.colorLong).isNotEqualTo(originalBrush.colorLong)
242         assertThat(newBrush.colorIntArgb).isEqualTo(0xAA123456.toInt())
243 
244         // The new brush has the original family, size and epsilon.
245         assertThat(newBrush.family).isSameInstanceAs(originalBrush.family)
246         assertThat(newBrush.size).isEqualTo(originalBrush.size)
247         assertThat(newBrush.epsilon).isEqualTo(originalBrush.epsilon)
248     }
249 
250     @Test
copyWithColorLong_withChangedColor_returnsCopyWithThatColornull251     fun copyWithColorLong_withChangedColor_returnsCopyWithThatColor() {
252         val originalBrush = buildTestBrush()
253 
254         val newColor = Color(red = 255, green = 230, blue = 115, alpha = 140).value.toLong()
255         val newBrush = originalBrush.copyWithColorLong(colorLong = newColor)
256 
257         assertThat(newBrush).isNotEqualTo(originalBrush)
258         assertThat(newBrush.colorLong).isNotEqualTo(originalBrush.colorLong)
259         assertThat(newBrush.colorLong).isEqualTo(newColor)
260 
261         // The new brush has the original family, size and epsilon.
262         assertThat(newBrush.family).isSameInstanceAs(originalBrush.family)
263         assertThat(newBrush.size).isEqualTo(originalBrush.size)
264         assertThat(newBrush.epsilon).isEqualTo(originalBrush.epsilon)
265     }
266 
267     @Test
copyWithColorLong_inUnsupportedColorSpace_returnsCopyWithConvertedColornull268     fun copyWithColorLong_inUnsupportedColorSpace_returnsCopyWithConvertedColor() {
269         val originalBrush = buildTestBrush()
270 
271         val newColor = Color(0.9f, 0.45f, 0.55f, 0.15f, ColorSpaces.AdobeRgb).value.toLong()
272         val newBrush = originalBrush.copyWithColorLong(colorLong = newColor)
273 
274         val expectedColor = Color(newColor.toULong()).convert(ColorSpaces.DisplayP3)
275         assertThat(newBrush.colorLong).isEqualTo(expectedColor.value.toLong())
276         assertThat(newBrush.colorIntArgb).isEqualTo(expectedColor.toArgb())
277     }
278 
279     @Test
brushBuilderBuild_withColorIntWithLowAlpha_createsExpectedBrushnull280     fun brushBuilderBuild_withColorIntWithLowAlpha_createsExpectedBrush() {
281         val testBrush = buildTestBrush()
282 
283         val builtBrush =
284             Brush.builder()
285                 .setFamily(testBrush.family)
286                 .setColorIntArgb(0x12345678)
287                 .setSize(9f)
288                 .setEpsilon(0.9f)
289                 .build()
290 
291         assertThat(builtBrush.family).isEqualTo(testBrush.family)
292         assertThat(builtBrush.colorIntArgb).isEqualTo(0x12345678)
293         assertThat(builtBrush.size).isEqualTo(9f)
294         assertThat(builtBrush.epsilon).isEqualTo(0.9f)
295     }
296 
297     @Test
brushBuilderBuild_withColorIntWithHighAlpha_createsExpectedBrushnull298     fun brushBuilderBuild_withColorIntWithHighAlpha_createsExpectedBrush() {
299         val testBrush = buildTestBrush()
300 
301         val builtBrush =
302             Brush.builder()
303                 .setFamily(testBrush.family)
304                 .setColorIntArgb(0xAA123456.toInt())
305                 .setSize(9f)
306                 .setEpsilon(0.9f)
307                 .build()
308 
309         assertThat(builtBrush.family).isEqualTo(testBrush.family)
310         assertThat(builtBrush.colorIntArgb).isEqualTo(0xAA123456.toInt())
311         assertThat(builtBrush.size).isEqualTo(9f)
312         assertThat(builtBrush.epsilon).isEqualTo(0.9f)
313     }
314 
315     @Test
brushBuilderBuild_withColorLong_createsExpectedBrushnull316     fun brushBuilderBuild_withColorLong_createsExpectedBrush() {
317         val testBrush = buildTestBrush()
318         val testColorLong = Color(0.9f, 0.45f, 0.55f, 0.15f, ColorSpaces.DisplayP3).value.toLong()
319 
320         val builtBrush =
321             Brush.builder()
322                 .setFamily(testBrush.family)
323                 .setColorLong(testColorLong)
324                 .setSize(9f)
325                 .setEpsilon(0.9f)
326                 .build()
327 
328         assertThat(builtBrush.family).isEqualTo(testBrush.family)
329         assertThat(builtBrush.colorLong).isEqualTo(testColorLong)
330         assertThat(builtBrush.size).isEqualTo(9f)
331         assertThat(builtBrush.epsilon).isEqualTo(0.9f)
332     }
333 
334     @Test
brushBuilderBuild_withUnsupportedColorSpace_createsBrushWithConvertedColornull335     fun brushBuilderBuild_withUnsupportedColorSpace_createsBrushWithConvertedColor() {
336         val testBrush = buildTestBrush()
337         val testColorLong = Color(0.9f, 0.45f, 0.55f, 0.15f, ColorSpaces.AdobeRgb).value.toLong()
338 
339         val builtBrush =
340             Brush.builder()
341                 .setFamily(testBrush.family)
342                 .setColorLong(testColorLong)
343                 .setSize(9f)
344                 .setEpsilon(0.9f)
345                 .build()
346 
347         val expectedColor = Color(testColorLong.toULong()).convert(ColorSpaces.DisplayP3)
348         assertThat(builtBrush.family).isEqualTo(testBrush.family)
349         assertThat(builtBrush.colorLong).isEqualTo(expectedColor.value.toLong())
350         assertThat(builtBrush.size).isEqualTo(9f)
351         assertThat(builtBrush.epsilon).isEqualTo(0.9f)
352     }
353 
354     /**
355      * Creates an expected C++ Brush with default brush family/color and returns true if every
356      * property of the Kotlin Brush's JNI-created C++ counterpart is equivalent to the expected C++
357      * Brush.
358      */
matchesDefaultBrushnull359     @UsedByNative private external fun matchesDefaultBrush(actualBrushNativePointer: Long): Boolean
360 
361     /**
362      * Creates an expected C++ Brush with custom values and returns true if every property of the
363      * Kotlin Brush's JNI-created C++ counterpart is equivalent to the expected C++ Brush.
364      */
365     @UsedByNative private external fun matchesCustomBrush(actualBrushNativePointer: Long): Boolean
366 
367     /** Brush with every field different from default values. */
368     private fun buildTestBrush(): Brush =
369         Brush(
370             BrushFamily(
371                 tip =
372                     BrushTip(
373                         0.1f,
374                         0.2f,
375                         0.3f,
376                         0.4f,
377                         0.5f,
378                         0.6f,
379                         0.7f,
380                         0.8f,
381                         9L,
382                         listOf(
383                             BrushBehavior(
384                                 source = BrushBehavior.Source.TILT_IN_RADIANS,
385                                 target = BrushBehavior.Target.HEIGHT_MULTIPLIER,
386                                 sourceValueRangeStart = 0.2f,
387                                 sourceValueRangeEnd = .8f,
388                                 targetModifierRangeStart = 1.1f,
389                                 targetModifierRangeEnd = 1.7f,
390                                 sourceOutOfRangeBehavior = BrushBehavior.OutOfRange.MIRROR,
391                                 responseCurve = EasingFunction.Predefined.EASE_IN_OUT,
392                                 responseTimeMillis = 1L,
393                                 enabledToolTypes = setOf(InputToolType.STYLUS),
394                                 isFallbackFor = BrushBehavior.OptionalInputProperty.TILT_X_AND_Y,
395                             )
396                         ),
397                     ),
398                 paint = BrushPaint(),
399                 clientBrushFamilyId = "/brush-family:marker:1",
400             ),
401             color,
402             13F,
403             0.1234F,
404         )
405 }
406