1 /* <lambda>null2 * 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.annotation.ColorInt 20 import androidx.annotation.ColorLong 21 import androidx.annotation.FloatRange 22 import androidx.annotation.RestrictTo 23 import androidx.ink.brush.color.Color as ComposeColor 24 import androidx.ink.brush.color.toArgb 25 import androidx.ink.nativeloader.NativeLoader 26 import androidx.ink.nativeloader.UsedByNative 27 import kotlin.Float 28 import kotlin.jvm.JvmStatic 29 30 /** 31 * Defines how stroke inputs are interpreted to create the visual representation of a stroke. 32 * 33 * The type completely describes how inputs are used to create stroke meshes, and how those meshes 34 * should be drawn by stroke renderers. In an analogous way to "font" and "font family", a [Brush] 35 * can be considered an instance of a [BrushFamily] with a particular [color], [size], and an extra 36 * parameter controlling visual fidelity, called [epsilon]. 37 */ 38 @Suppress("NotCloseable") // Finalize is only used to free the native peer. 39 public class Brush 40 private constructor( 41 /** A handle to the underlying native [Brush] object. */ 42 @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public val nativePointer: Long, 43 44 /** The [BrushFamily] for this brush. See [StockBrushes] for available [BrushFamily] values. */ 45 public val family: BrushFamily, 46 ) { 47 48 @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 49 public val composeColor: ComposeColor = 50 // Caching this because the native call is slow. Still doing the round-trip on construction 51 // to 52 // ensure this is excercised by tests and that deserialized brushes are consistent with 53 // newly 54 // constructed brushes. 55 ComposeColor(BrushNative.computeComposeColorLong(nativePointer).toULong()) 56 57 /** 58 * The overall thickness of strokes created with a given brush, in the same units as the stroke 59 * coordinate system. This must be at least as big as [epsilon]. 60 */ 61 @get:FloatRange( 62 from = 0.0, 63 fromInclusive = false, 64 to = Double.POSITIVE_INFINITY, 65 toInclusive = false, 66 ) 67 public val size: Float 68 get() = BrushNative.getSize(nativePointer) 69 70 /** 71 * The smallest distance for which two points should be considered visually distinct for stroke 72 * generation geometry purposes. Effectively, it is the visual fidelity of strokes created with 73 * this brush, where any (lack of) visual fidelity can be observed by a user the further zoomed 74 * in they are on the stroke. Lower values of [epsilon] result in higher fidelity strokes at the 75 * cost of somewhat higher memory usage. This value, like [size], is in the same units as the 76 * stroke coordinate system. A size of 0.1 physical pixels at the default zoom level is a good 77 * starting point that can tolerate a reasonable amount of zooming in with high quality visual 78 * results. 79 */ 80 @get:FloatRange( 81 from = 0.0, 82 fromInclusive = false, 83 to = Double.POSITIVE_INFINITY, 84 toInclusive = false, 85 ) 86 public val epsilon: Float 87 get() = BrushNative.getEpsilon(nativePointer) 88 89 internal constructor( 90 family: BrushFamily, 91 composeColor: ComposeColor, 92 @FloatRange( 93 from = 0.0, 94 fromInclusive = false, 95 to = Double.POSITIVE_INFINITY, 96 toInclusive = false, 97 ) 98 size: Float, 99 @FloatRange( 100 from = 0.0, 101 fromInclusive = false, 102 to = Double.POSITIVE_INFINITY, 103 toInclusive = false, 104 ) 105 epsilon: Float, 106 ) : this( 107 composeColor.toColorInInkSupportedColorSpace().let { convertedColor -> 108 BrushNative.create( 109 family.nativePointer, 110 convertedColor.red, 111 convertedColor.green, 112 convertedColor.blue, 113 convertedColor.alpha, 114 convertedColor.colorSpace.toInkColorSpaceId(), 115 size, 116 epsilon, 117 ) 118 }, 119 family, 120 ) 121 122 /** 123 * The default color of a [Brush] is pure black. To set a custom color, use 124 * [createWithColorLong] or [createWithColorIntArgb]. 125 */ 126 public constructor( 127 family: BrushFamily, 128 @FloatRange( 129 from = 0.0, 130 fromInclusive = false, 131 to = Double.POSITIVE_INFINITY, 132 toInclusive = false, 133 ) 134 size: Float, 135 @FloatRange( 136 from = 0.0, 137 fromInclusive = false, 138 to = Double.POSITIVE_INFINITY, 139 toInclusive = false, 140 ) 141 epsilon: Float, 142 ) : this(family, DEFAULT_COMPOSE_COLOR, size, epsilon) 143 144 /** 145 * The brush color as a [ColorLong], which can express colors in several different color spaces. 146 * sRGB and Display P3 are supported; a color in any other color space will be converted to 147 * Display P3. 148 */ 149 public val colorLong: Long 150 @ColorLong get(): Long = composeColor.value.toLong() 151 152 /** 153 * The brush color as a [ColorInt], which can only express colors in the sRGB color space. For 154 * clients that want to support wide-gamut colors, use [colorLong]. 155 */ 156 public val colorIntArgb: Int 157 @ColorInt get(): Int = composeColor.toArgb() 158 159 // Base implementation of copy() that all public versions call. 160 private fun copy(family: BrushFamily, color: ComposeColor, size: Float, epsilon: Float): Brush { 161 return if ( 162 family == this.family && 163 color == this.composeColor && 164 size == this.size && 165 epsilon == this.epsilon 166 ) { 167 // For a pure copy, return the same object, since it is immutable. 168 this 169 } else { 170 Brush(family, color, size, epsilon) 171 } 172 } 173 174 /** 175 * Creates a copy of `this` and allows named properties to be altered while keeping the rest 176 * unchanged. To change the color, use [copyWithColorLong] or [copyWithColorIntArgb]. 177 */ 178 @JvmOverloads 179 public fun copy( 180 family: BrushFamily = this.family, 181 @FloatRange( 182 from = 0.0, 183 fromInclusive = false, 184 to = Double.POSITIVE_INFINITY, 185 toInclusive = false, 186 ) 187 size: Float = this.size, 188 @FloatRange( 189 from = 0.0, 190 fromInclusive = false, 191 to = Double.POSITIVE_INFINITY, 192 toInclusive = false, 193 ) 194 epsilon: Float = this.epsilon, 195 ): Brush = copy(family, this.composeColor, size, epsilon) 196 197 /** 198 * Creates a copy of `this` and allows named properties to be altered while keeping the rest 199 * unchanged. The color is specified as a [ColorLong], which can encode several different color 200 * spaces. sRGB and Display P3 are supported; a color in any other color space will be converted 201 * to Display P3. 202 * 203 * Some libraries (notably Jetpack UI Graphics) use [ULong] for [ColorLong]s, so the caller must 204 * call [ULong.toLong] on such a value before passing it to this method. 205 */ 206 @JvmOverloads 207 public fun copyWithColorLong( 208 @ColorLong colorLong: Long, 209 family: BrushFamily = this.family, 210 @FloatRange( 211 from = 0.0, 212 fromInclusive = false, 213 to = Double.POSITIVE_INFINITY, 214 toInclusive = false, 215 ) 216 size: Float = this.size, 217 @FloatRange( 218 from = 0.0, 219 fromInclusive = false, 220 to = Double.POSITIVE_INFINITY, 221 toInclusive = false, 222 ) 223 epsilon: Float = this.epsilon, 224 ): Brush = copy(family, ComposeColor(colorLong.toULong()), size, epsilon) 225 226 /** 227 * Creates a copy of `this` and allows named properties to be altered while keeping the rest 228 * unchanged. The color is specified as a [ColorInt], which is in the sRGB color space by 229 * definition. Note that the [ColorInt] channel order puts alpha first (in the most significant 230 * byte). 231 * 232 * Kotlin interprets integer literals greater than `0x7fffffff` as [Long]s, so callers that want 233 * to specify a literal [ColorInt] with alpha >= 0x80 must call [Long.toInt] on the literal. 234 */ 235 @JvmOverloads 236 public fun copyWithColorIntArgb( 237 @ColorInt colorIntArgb: Int, 238 family: BrushFamily = this.family, 239 @FloatRange( 240 from = 0.0, 241 fromInclusive = false, 242 to = Double.POSITIVE_INFINITY, 243 toInclusive = false, 244 ) 245 size: Float = this.size, 246 @FloatRange( 247 from = 0.0, 248 fromInclusive = false, 249 to = Double.POSITIVE_INFINITY, 250 toInclusive = false, 251 ) 252 epsilon: Float = this.epsilon, 253 ): Brush = copy(family, ComposeColor(colorIntArgb), size, epsilon) 254 255 /** 256 * Returns a [Builder] with values set equivalent to `this`. Java developers, use the returned 257 * builder to build a copy of a Brush. Kotlin developers, see [copy] method. 258 */ 259 public fun toBuilder(): Builder = 260 Builder().setFamily(family).setComposeColor(composeColor).setSize(size).setEpsilon(epsilon) 261 262 /** 263 * Builder for [Brush]. 264 * 265 * Use Brush.Builder to construct a [Brush] with default values, overriding only as needed. 266 */ 267 public class Builder { 268 private var family: BrushFamily? = null 269 private var composeColor: ComposeColor = DEFAULT_COMPOSE_COLOR 270 271 @FloatRange( 272 from = 0.0, 273 fromInclusive = false, 274 to = Double.POSITIVE_INFINITY, 275 toInclusive = false, 276 ) 277 private var size: Float? = null 278 279 @FloatRange( 280 from = 0.0, 281 fromInclusive = false, 282 to = Double.POSITIVE_INFINITY, 283 toInclusive = false, 284 ) 285 private var epsilon: Float? = null 286 287 /** 288 * Sets the [BrushFamily] for this brush. See [StockBrushes] for available [BrushFamily] 289 * values. 290 */ 291 public fun setFamily(family: BrushFamily): Builder { 292 this.family = family 293 return this 294 } 295 296 internal fun setComposeColor(color: ComposeColor): Builder { 297 this.composeColor = color 298 return this 299 } 300 301 /** 302 * Sets the color using a [ColorLong], which can encode several different color spaces. sRGB 303 * and Display P3 are supported; a color in any other color space will be converted to 304 * Display P3. 305 * 306 * Some libraries (notably Jetpack UI Graphics) use [ULong] for [ColorLong]s, so the caller 307 * must call [ULong.toLong] on such a value before passing it to this method. 308 */ 309 public fun setColorLong(@ColorLong colorLong: Long): Builder { 310 this.composeColor = ComposeColor(colorLong.toULong()) 311 return this 312 } 313 314 /** 315 * Sets the color using a [ColorInt], which is in the sRGB color space by definition. Note 316 * that the [ColorInt] channel order puts alpha first (in the most significant byte). 317 * 318 * Kotlin interprets integer literals greater than `0x7fffffff` as [Long]s, so Kotlin 319 * callers that want to specify a literal [ColorInt] with alpha >= 0x80 must call 320 * [Long.toInt] on the literal. 321 */ 322 public fun setColorIntArgb(@ColorInt colorIntArgb: Int): Builder { 323 this.composeColor = ComposeColor(colorIntArgb) 324 return this 325 } 326 327 public fun setSize( 328 @FloatRange( 329 from = 0.0, 330 fromInclusive = false, 331 to = Double.POSITIVE_INFINITY, 332 toInclusive = false, 333 ) 334 size: Float 335 ): Builder { 336 this.size = size 337 return this 338 } 339 340 public fun setEpsilon( 341 @FloatRange( 342 from = 0.0, 343 fromInclusive = false, 344 to = Double.POSITIVE_INFINITY, 345 toInclusive = false, 346 ) 347 epsilon: Float 348 ): Builder { 349 this.epsilon = epsilon 350 return this 351 } 352 353 public fun build(): Brush = 354 Brush( 355 family = 356 checkNotNull(family) { 357 "brush family must be specified before calling build()" 358 }, 359 composeColor = composeColor, 360 size = checkNotNull(size) { "brush size must be specified before calling build()" }, 361 epsilon = 362 checkNotNull(epsilon) { 363 "brush epsilon must be specified before calling build()" 364 }, 365 ) 366 } 367 368 override fun equals(other: Any?): Boolean { 369 if (this === other) return true 370 if (other !is Brush) return false 371 372 if (family != other.family) return false 373 if (composeColor != other.composeColor) return false 374 if (size != other.size) return false 375 if (epsilon != other.epsilon) return false 376 377 return true 378 } 379 380 // NOMUTANTS -- not testing exact hashCode values, just that equality implies the same hashCode. 381 override fun hashCode(): Int { 382 var result = family.hashCode() 383 result = 31 * result + composeColor.hashCode() 384 result = 31 * result + size.hashCode() 385 result = 31 * result + epsilon.hashCode() 386 return result 387 } 388 389 override fun toString(): String { 390 return "Brush(family=$family, color=$composeColor, size=$size, epsilon=$epsilon)" 391 } 392 393 /** Delete native Brush memory. */ 394 protected fun finalize() { 395 // NOMUTANTS -- Not tested post garbage collection. 396 BrushNative.free(nativePointer) 397 } 398 399 public companion object { 400 private val DEFAULT_COMPOSE_COLOR = ComposeColor.Black 401 402 /** 403 * Construct a [BrushPaint] from an unowned heap-allocated native pointer to a C++ 404 * `BrushPaint`. Kotlin wrapper objects nested under the [BrushPaint] are initialized 405 * similarly using their own [wrapNative] methods, passing those pointers to newly 406 * copy-constructed heap-allocated objects. That avoids the need to call Kotlin constructors 407 * for those objects from C++. 408 */ 409 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 410 public fun wrapNative(unownedNativePointer: Long): Brush { 411 return Brush( 412 unownedNativePointer, 413 BrushFamily.wrapNative(BrushNative.newCopyOfBrushFamily(unownedNativePointer)), 414 ) 415 } 416 417 /** 418 * Returns a new [Brush] with the color specified by a [ColorLong], which can encode several 419 * different color spaces. sRGB and Display P3 are supported; a color in any other color 420 * space will be converted to Display P3. 421 * 422 * Some libraries (notably Jetpack UI Graphics) use [ULong] for [ColorLong]s, so the caller 423 * must call [ULong.toLong] on such a value before passing it to this method. 424 */ 425 @JvmStatic 426 public fun createWithColorLong( 427 family: BrushFamily, 428 @ColorLong colorLong: Long, 429 size: Float, 430 epsilon: Float, 431 ): Brush = Brush(family, ComposeColor(colorLong.toULong()), size, epsilon) 432 433 /** 434 * Returns a new [Brush] with the color specified by a [ColorInt], which is in the sRGB 435 * color space by definition. Note that the [ColorInt] channel order puts alpha first (in 436 * the most significant byte). 437 * 438 * Kotlin interprets integer literals greater than `0x7fffffff` as [Long]s, so callers that 439 * want to specify a literal [ColorInt] with alpha >= 0x80 must call [Long.toInt] on the 440 * literal. 441 */ 442 @JvmStatic 443 public fun createWithColorIntArgb( 444 family: BrushFamily, 445 @ColorInt colorIntArgb: Int, 446 size: Float, 447 epsilon: Float, 448 ): Brush = Brush(family, ComposeColor(colorIntArgb), size, epsilon) 449 450 /** Returns a new [Brush.Builder]. */ 451 @JvmStatic public fun builder(): Builder = Builder() 452 } 453 } 454 455 /** Singleton wrapper around native JNI calls. */ 456 @UsedByNative 457 private object BrushNative { 458 init { 459 NativeLoader.load() 460 } 461 462 /** Create underlying native object and return reference for all subsequent native calls. */ 463 @UsedByNative createnull464 public external fun create( 465 familyNativePointer: Long, 466 colorRed: Float, 467 colorGreen: Float, 468 colorBlue: Float, 469 colorAlpha: Float, 470 colorSpace: Int, 471 size: Float, 472 epsilon: Float, 473 ): Long 474 475 /** Release the underlying memory allocated in [create]. */ 476 @UsedByNative public external fun free(nativePointer: Long) 477 478 @UsedByNative public external fun computeComposeColorLong(nativePointer: Long): Long 479 480 /** This is a callback used by computeComposeColorLong. */ 481 @UsedByNative 482 public fun composeColorLongFromComponents( 483 colorSpaceId: Int, 484 redGammaCorrected: Float, 485 greenGammaCorrected: Float, 486 blueGammaCorrected: Float, 487 alpha: Float, 488 ): Long = 489 ComposeColor( 490 redGammaCorrected, 491 greenGammaCorrected, 492 blueGammaCorrected, 493 alpha, 494 colorSpace = composeColorSpaceFromInkColorSpaceId(colorSpaceId), 495 ) 496 .value 497 .toLong() 498 499 @UsedByNative public external fun getSize(nativePointer: Long): Float 500 501 @UsedByNative public external fun getEpsilon(nativePointer: Long): Float 502 503 /** 504 * Returns a new, unowned native pointer to a copy of the `BrushFamily` for the pointed-at 505 * native `Brush`. 506 */ 507 @UsedByNative public external fun newCopyOfBrushFamily(nativePointer: Long): Long 508 } 509