1 /*
2  * Copyright 2020 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.graphics.drawscope
18 
19 import androidx.annotation.FloatRange
20 import androidx.compose.ui.geometry.CornerRadius
21 import androidx.compose.ui.geometry.Offset
22 import androidx.compose.ui.geometry.Size
23 import androidx.compose.ui.geometry.center
24 import androidx.compose.ui.graphics.BlendMode
25 import androidx.compose.ui.graphics.Brush
26 import androidx.compose.ui.graphics.Canvas
27 import androidx.compose.ui.graphics.ClipOp
28 import androidx.compose.ui.graphics.Color
29 import androidx.compose.ui.graphics.ColorFilter
30 import androidx.compose.ui.graphics.FilterQuality
31 import androidx.compose.ui.graphics.ImageBitmap
32 import androidx.compose.ui.graphics.Paint
33 import androidx.compose.ui.graphics.Path
34 import androidx.compose.ui.graphics.PathEffect
35 import androidx.compose.ui.graphics.PointMode
36 import androidx.compose.ui.graphics.StrokeCap
37 import androidx.compose.ui.graphics.StrokeJoin
38 import androidx.compose.ui.graphics.degrees
39 import androidx.compose.ui.graphics.internal.JvmDefaultWithCompatibility
40 import androidx.compose.ui.graphics.layer.GraphicsLayer
41 import androidx.compose.ui.unit.Density
42 import androidx.compose.ui.unit.IntOffset
43 import androidx.compose.ui.unit.IntSize
44 import androidx.compose.ui.unit.LayoutDirection
45 import androidx.compose.ui.unit.toIntSize
46 
47 /**
48  * Simultaneously translate the [DrawScope] coordinate space by [left] and [top] as well as modify
49  * the dimensions of the current painting area. This provides a callback to issue more drawing
50  * instructions within the modified coordinate space. This method modifies the width of the
51  * [DrawScope] to be equivalent to width - (left + right) as well as height to height - (top +
52  * bottom). After this method is invoked, the coordinate space is returned to the state before the
53  * inset was applied.
54  *
55  * @param left number of pixels to inset the left drawing bound
56  * @param top number of pixels to inset the top drawing bound
57  * @param right number of pixels to inset the right drawing bound
58  * @param bottom number of pixels to inset the bottom drawing bound
59  * @param block lambda that is called to issue drawing commands within the inset coordinate space
60  */
insetnull61 inline fun DrawScope.inset(
62     left: Float,
63     top: Float,
64     right: Float,
65     bottom: Float,
66     block: DrawScope.() -> Unit
67 ) {
68     drawContext.transform.inset(left, top, right, bottom)
69     try {
70         block()
71     } finally {
72         drawContext.transform.inset(-left, -top, -right, -bottom)
73     }
74 }
75 
76 /**
77  * Convenience method modifies the [DrawScope] bounds to inset both left, top, right and bottom
78  * bounds by [inset]. After this method is invoked, the coordinate space is returned to the state
79  * before this inset was applied.
80  *
81  * @param inset number of pixels to inset left, top, right, and bottom bounds.
82  * @param block lambda that is called to issue additional drawing commands within the modified
83  *   coordinate space
84  */
insetnull85 inline fun DrawScope.inset(inset: Float, block: DrawScope.() -> Unit) {
86     drawContext.transform.inset(inset, inset, inset, inset)
87     try {
88         block()
89     } finally {
90         drawContext.transform.inset(-inset, -inset, -inset, -inset)
91     }
92 }
93 
94 /**
95  * Convenience method modifies the [DrawScope] bounds to inset both left and right bounds by
96  * [horizontal] as well as the top and bottom by [vertical]. After this method is invoked, the
97  * coordinate space is returned to the state before this inset was applied.
98  *
99  * @param horizontal number of pixels to inset both left and right bounds. Zero by default
100  * @param vertical Optional number of pixels to inset both top and bottom bounds. Zero by default
101  * @param block lambda that is called to issue additional drawing commands within the modified
102  *   coordinate space
103  */
insetnull104 inline fun DrawScope.inset(
105     horizontal: Float = 0.0f,
106     vertical: Float = 0.0f,
107     block: DrawScope.() -> Unit
108 ) = inset(horizontal, vertical, horizontal, vertical, block)
109 
110 /**
111  * Translate the coordinate space by the given delta in pixels in both the x and y coordinates
112  * respectively
113  *
114  * @param left Pixels to translate the coordinate space in the x-axis
115  * @param top Pixels to translate the coordinate space in the y-axis
116  * @param block lambda that is called to issue drawing commands within the translated coordinate
117  *   space
118  */
119 inline fun DrawScope.translate(left: Float = 0.0f, top: Float = 0.0f, block: DrawScope.() -> Unit) {
120     drawContext.transform.translate(left, top)
121     try {
122         block()
123     } finally {
124         drawContext.transform.translate(-left, -top)
125     }
126 }
127 
128 /**
129  * Add a rotation (in degrees clockwise) to the current transform at the given pivot point. The
130  * pivot coordinate remains unchanged by the rotation transformation. After the provided lambda is
131  * invoked, the rotation transformation is undone.
132  *
133  * @param degrees to rotate clockwise
134  * @param pivot The coordinate for the pivot point, defaults to the center of the coordinate space
135  * @param block lambda that is called to issue drawing commands within the rotated coordinate space
136  */
rotatenull137 inline fun DrawScope.rotate(degrees: Float, pivot: Offset = center, block: DrawScope.() -> Unit) =
138     withTransform({ rotate(degrees, pivot) }, block)
139 
140 /**
141  * Add a rotation (in radians clockwise) to the current transform at the given pivot point. The
142  * pivot coordinate remains unchanged by the rotation transformation
143  *
144  * @param radians to rotate clockwise
145  * @param pivot The coordinate for the pivot point, defaults to the center of the coordinate space
146  * @param block lambda that is called to issue drawing commands within the rotated coordinate space
147  */
rotateRadnull148 inline fun DrawScope.rotateRad(
149     radians: Float,
150     pivot: Offset = center,
151     block: DrawScope.() -> Unit
152 ) {
153     withTransform({ rotate(degrees(radians), pivot) }, block)
154 }
155 
156 /**
157  * Add an axis-aligned scale to the current transform, scaling by the first argument in the
158  * horizontal direction and the second in the vertical direction at the given pivot coordinate. The
159  * pivot coordinate remains unchanged by the scale transformation. After this method is invoked, the
160  * coordinate space is returned to the state before the scale was applied.
161  *
162  * @param scaleX The amount to scale in X
163  * @param scaleY The amount to scale in Y
164  * @param pivot The coordinate for the pivot point, defaults to the center of the coordinate space
165  * @param block lambda used to issue drawing commands within the scaled coordinate space
166  */
scalenull167 inline fun DrawScope.scale(
168     scaleX: Float,
169     scaleY: Float,
170     pivot: Offset = center,
171     block: DrawScope.() -> Unit
172 ) = withTransform({ scale(scaleX, scaleY, pivot) }, block)
173 
174 /**
175  * Add an axis-aligned scale to the current transform, scaling both the horizontal direction and the
176  * vertical direction at the given pivot coordinate. The pivot coordinate remains unchanged by the
177  * scale transformation. After this method is invoked, the coordinate space is returned to the state
178  * before the scale was applied.
179  *
180  * @param scale The amount to scale uniformly in both directions
181  * @param pivot The coordinate for the pivot point, defaults to the center of the coordinate space
182  * @param block lambda used to issue drawing commands within the scaled coordinate space
183  */
scalenull184 inline fun DrawScope.scale(scale: Float, pivot: Offset = center, block: DrawScope.() -> Unit) =
185     withTransform({ scale(scale, scale, pivot) }, block)
186 
187 /**
188  * Reduces the clip region to the intersection of the current clip and the given rectangle indicated
189  * by the given left, top, right and bottom bounds. This provides a callback to issue drawing
190  * commands within the clipped region. After this method is invoked, this clip is no longer applied.
191  *
192  * Use [ClipOp.Difference] to subtract the provided rectangle from the current clip.
193  *
194  * @param left Left bound of the rectangle to clip
195  * @param top Top bound of the rectangle to clip
196  * @param right Right bound of the rectangle to clip
197  * @param bottom Bottom bound of the rectangle to clip
198  * @param clipOp Clipping operation to conduct on the given bounds, defaults to [ClipOp.Intersect]
199  * @param block Lambda callback with this CanvasScope as a receiver scope to issue drawing commands
200  *   within the provided clip
201  */
clipRectnull202 inline fun DrawScope.clipRect(
203     left: Float = 0.0f,
204     top: Float = 0.0f,
205     right: Float = size.width,
206     bottom: Float = size.height,
207     clipOp: ClipOp = ClipOp.Intersect,
208     block: DrawScope.() -> Unit
209 ) = withTransform({ clipRect(left, top, right, bottom, clipOp) }, block)
210 
211 /**
212  * Reduces the clip region to the intersection of the current clip and the given path. This method
213  * provides a callback to issue drawing commands within the region defined by the clipped path.
214  * After this method is invoked, this clip is no longer applied.
215  *
216  * @param path Shape to clip drawing content within
217  * @param clipOp Clipping operation to conduct on the given bounds, defaults to [ClipOp.Intersect]
218  * @param block Lambda callback with this CanvasScope as a receiver scope to issue drawing commands
219  *   within the provided clip
220  */
clipPathnull221 inline fun DrawScope.clipPath(
222     path: Path,
223     clipOp: ClipOp = ClipOp.Intersect,
224     block: DrawScope.() -> Unit
225 ) = withTransform({ clipPath(path, clipOp) }, block)
226 
227 /**
228  * Provides access to draw directly with the underlying [Canvas]. This is helpful for situations to
229  * re-use alternative drawing logic in combination with [DrawScope]
230  *
231  * @param block Lambda callback to issue drawing commands on the provided [Canvas]
232  */
drawIntoCanvasnull233 inline fun DrawScope.drawIntoCanvas(block: (Canvas) -> Unit) = block(drawContext.canvas)
234 
235 /**
236  * Perform 1 or more transformations and execute drawing commands with the specified transformations
237  * applied. After this call is complete, the transformation before this call was made is restored
238  *
239  * @sample androidx.compose.ui.graphics.samples.DrawScopeBatchedTransformSample
240  * @param transformBlock Callback invoked to issue transformations to be made before the drawing
241  *   operations are issued
242  * @param drawBlock Callback invoked to issue drawing operations after the transformations are
243  *   applied
244  */
245 inline fun DrawScope.withTransform(
246     transformBlock: DrawTransform.() -> Unit,
247     drawBlock: DrawScope.() -> Unit
248 ) =
249     with(drawContext) {
250         // Transformation can include inset calls which change the drawing area
251         // so cache the previous size before the transformation is done
252         // and reset it afterwards
253         val previousSize = size
254         canvas.save()
255         try {
256             transformBlock(transform)
257             drawBlock()
258         } finally {
259             canvas.restore()
260             size = previousSize
261         }
262     }
263 
264 @Deprecated(
265     message = "Please use a new overload accepting nullable GraphicsLayer",
266     level = DeprecationLevel.HIDDEN
267 )
drawnull268 inline fun DrawScope.draw(
269     density: Density,
270     layoutDirection: LayoutDirection,
271     canvas: Canvas,
272     size: Size,
273     block: DrawScope.() -> Unit
274 ) {
275     draw(density, layoutDirection, canvas, size, null, block)
276 }
277 
278 /**
279  * Draws into the provided [Canvas] with the commands specified in the lambda with this [DrawScope]
280  * as a receiver
281  *
282  * @sample androidx.compose.ui.graphics.samples.DrawScopeRetargetingSample
283  * @param density [Density] used to assist in conversions of density independent pixels to raw
284  *   pixels to draw
285  * @param layoutDirection [LayoutDirection] of the layout being drawn in.
286  * @param canvas target canvas to render into
287  * @param size bounds relative to the current canvas translation in which the [DrawScope] should
288  *   draw within
289  * @param graphicsLayer Current [GraphicsLayer] we are drawing into. Might be null if the [canvas]
290  *   is not provided by a [GraphicsLayer], for example in the case of a software-accelerated drawing
291  * @param block lambda that is called to issue drawing commands on this [DrawScope]
292  */
drawnull293 inline fun DrawScope.draw(
294     density: Density,
295     layoutDirection: LayoutDirection,
296     canvas: Canvas,
297     size: Size,
298     graphicsLayer: GraphicsLayer? = null,
299     block: DrawScope.() -> Unit
300 ) {
301     // Remember the previous drawing parameters in case we are temporarily re-directing our
302     // drawing to a separate Layer/RenderNode only to draw that content back into the original
303     // Canvas. If there is no previous canvas that was being drawing into, this ends up
304     // resetting these parameters back to defaults defensively
305     val prevDensity = drawContext.density
306     val prevLayoutDirection = drawContext.layoutDirection
307     val prevCanvas = drawContext.canvas
308     val prevSize = drawContext.size
309     val prevLayer = drawContext.graphicsLayer
310     drawContext.apply {
311         this.density = density
312         this.layoutDirection = layoutDirection
313         this.canvas = canvas
314         this.size = size
315         this.graphicsLayer = graphicsLayer
316     }
317     canvas.save()
318     try {
319         this.block()
320     } finally {
321         canvas.restore()
322         drawContext.apply {
323             this.density = prevDensity
324             this.layoutDirection = prevLayoutDirection
325             this.canvas = prevCanvas
326             this.size = prevSize
327             this.graphicsLayer = prevLayer
328         }
329     }
330 }
331 
332 /**
333  * Creates a scoped drawing environment with the provided [Canvas]. This provides a declarative,
334  * stateless API to draw shapes and paths without requiring consumers to maintain underlying
335  * [Canvas] state information. [DrawScope] implementations are also provided sizing information and
336  * transformations are done relative to the local translation. That is left and top coordinates are
337  * always the origin and the right and bottom coordinates are always the specified width and height
338  * respectively. Drawing content is not clipped, so it is possible to draw outside of the specified
339  * bounds.
340  *
341  * @sample androidx.compose.ui.graphics.samples.DrawScopeSample
342  */
343 @DrawScopeMarker
344 @JvmDefaultWithCompatibility
345 interface DrawScope : Density {
346 
347     /**
348      * The current [DrawContext] that contains the dependencies needed to create the drawing
349      * environment
350      */
351     val drawContext: DrawContext
352 
353     /** Center of the current bounds of the drawing environment */
354     val center: Offset
355         get() = drawContext.size.center
356 
357     /** Provides the dimensions of the current drawing environment */
358     val size: Size
359         get() = drawContext.size
360 
361     /** The layout direction of the layout being drawn in. */
362     val layoutDirection: LayoutDirection
363 
364     /**
365      * Draws a line between the given points using the given paint. The line is stroked.
366      *
367      * @param brush the color or fill to be applied to the line
368      * @param start first point of the line to be drawn
369      * @param end second point of the line to be drawn
370      * @param strokeWidth stroke width to apply to the line
371      * @param cap treatment applied to the ends of the line segment
372      * @param pathEffect optional effect or pattern to apply to the line
373      * @param alpha opacity to be applied to the [brush] from 0.0f to 1.0f representing fully
374      *   transparent to fully opaque respectively
375      * @param colorFilter ColorFilter to apply to the [brush] when drawn into the destination
376      * @param blendMode the blending algorithm to apply to the [brush]
377      */
drawLinenull378     fun drawLine(
379         brush: Brush,
380         start: Offset,
381         end: Offset,
382         strokeWidth: Float = Stroke.HairlineWidth,
383         cap: StrokeCap = Stroke.DefaultCap,
384         pathEffect: PathEffect? = null,
385         @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
386         colorFilter: ColorFilter? = null,
387         blendMode: BlendMode = DefaultBlendMode
388     )
389 
390     /**
391      * Draws a line between the given points using the given paint. The line is stroked.
392      *
393      * @param color the color to be applied to the line
394      * @param start first point of the line to be drawn
395      * @param end second point of the line to be drawn
396      * @param strokeWidth The stroke width to apply to the line
397      * @param cap treatment applied to the ends of the line segment
398      * @param pathEffect optional effect or pattern to apply to the line
399      * @param alpha opacity to be applied to the [color] from 0.0f to 1.0f representing fully
400      *   transparent to fully opaque respectively
401      * @param colorFilter ColorFilter to apply to the [color] when drawn into the destination
402      * @param blendMode the blending algorithm to apply to the [color]
403      */
404     fun drawLine(
405         color: Color,
406         start: Offset,
407         end: Offset,
408         strokeWidth: Float = Stroke.HairlineWidth,
409         cap: StrokeCap = Stroke.DefaultCap,
410         pathEffect: PathEffect? = null,
411         @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
412         colorFilter: ColorFilter? = null,
413         blendMode: BlendMode = DefaultBlendMode
414     )
415 
416     /**
417      * Draws a rectangle with the given offset and size. If no offset from the top left is provided,
418      * it is drawn starting from the origin of the current translation. If no size is provided, the
419      * size of the current environment is used.
420      *
421      * @param brush The color or fill to be applied to the rectangle
422      * @param topLeft Offset from the local origin of 0, 0 relative to the current translation
423      * @param size Dimensions of the rectangle to draw
424      * @param alpha Opacity to be applied to the [brush] from 0.0f to 1.0f representing fully
425      *   transparent to fully opaque respectively
426      * @param style Whether or not the rectangle is stroked or filled in
427      * @param colorFilter ColorFilter to apply to the [brush] when drawn into the destination
428      * @param blendMode Blending algorithm to apply to destination
429      */
430     fun drawRect(
431         brush: Brush,
432         topLeft: Offset = Offset.Zero,
433         size: Size = this.size.offsetSize(topLeft),
434         @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
435         style: DrawStyle = Fill,
436         colorFilter: ColorFilter? = null,
437         blendMode: BlendMode = DefaultBlendMode
438     )
439 
440     /**
441      * Draws a rectangle with the given offset and size. If no offset from the top left is provided,
442      * it is drawn starting from the origin of the current translation. If no size is provided, the
443      * size of the current environment is used.
444      *
445      * @param color The color to be applied to the rectangle
446      * @param topLeft Offset from the local origin of 0, 0 relative to the current translation
447      * @param size Dimensions of the rectangle to draw
448      * @param alpha Opacity to be applied to the [color] from 0.0f to 1.0f representing fully
449      *   transparent to fully opaque respectively
450      * @param style Whether or not the rectangle is stroked or filled in
451      * @param colorFilter ColorFilter to apply to the [color] source pixels
452      * @param blendMode Blending algorithm to apply to destination
453      */
454     fun drawRect(
455         color: Color,
456         topLeft: Offset = Offset.Zero,
457         size: Size = this.size.offsetSize(topLeft),
458         @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
459         style: DrawStyle = Fill,
460         colorFilter: ColorFilter? = null,
461         blendMode: BlendMode = DefaultBlendMode
462     )
463 
464     /**
465      * Draws the given [ImageBitmap] into the canvas with its top-left corner at the given [Offset].
466      * The image is composited into the canvas using the given [Paint].
467      *
468      * @param image The [ImageBitmap] to draw
469      * @param topLeft Offset from the local origin of 0, 0 relative to the current translation
470      * @param alpha Opacity to be applied to [image] from 0.0f to 1.0f representing fully
471      *   transparent to fully opaque respectively
472      * @param style Specifies whether the image is to be drawn filled in or as a rectangular stroke
473      * @param colorFilter ColorFilter to apply to the [image] when drawn into the destination
474      * @param blendMode Blending algorithm to apply to destination
475      */
476     fun drawImage(
477         image: ImageBitmap,
478         topLeft: Offset = Offset.Zero,
479         @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
480         style: DrawStyle = Fill,
481         colorFilter: ColorFilter? = null,
482         blendMode: BlendMode = DefaultBlendMode
483     )
484 
485     /**
486      * Draws the subset of the given image described by the `src` argument into the canvas in the
487      * axis-aligned rectangle given by the `dst` argument.
488      *
489      * If no src rect is provided, the entire image is scaled into the corresponding destination
490      * bounds
491      *
492      * @param image The source image to draw
493      * @param srcOffset Optional offset representing the top left offset of the source image to
494      *   draw, this defaults to the origin of [image]
495      * @param srcSize Optional dimensions of the source image to draw relative to [srcOffset], this
496      *   defaults the width and height of [image]
497      * @param dstOffset Optional offset representing the top left offset of the destination to draw
498      *   the given image, this defaults to the origin of the current translation tarting top left
499      *   offset in the destination to draw the image
500      * @param dstSize Optional dimensions of the destination to draw, this defaults to [srcSize]
501      * @param alpha Opacity to be applied to [image] from 0.0f to 1.0f representing fully
502      *   transparent to fully opaque respectively
503      * @param style Specifies whether the image is to be drawn filled in or as a rectangular stroke
504      * @param colorFilter ColorFilter to apply to the [image] when drawn into the destination
505      * @param blendMode Blending algorithm to apply to destination
506      */
507     @Deprecated(
508         "Prefer usage of drawImage that consumes an optional FilterQuality parameter",
509         level = DeprecationLevel.HIDDEN,
510         replaceWith =
511             ReplaceWith(
512                 "drawImage(image, srcOffset, srcSize, dstOffset, dstSize, alpha, style, " +
513                     "colorFilter, blendMode, FilterQuality.Low)",
514                 "androidx.compose.ui.graphics.drawscope",
515                 "androidx.compose.ui.graphics.FilterQuality"
516             )
517     ) // Binary API compatibility.
518     fun drawImage(
519         image: ImageBitmap,
520         srcOffset: IntOffset = IntOffset.Zero,
521         srcSize: IntSize = IntSize(image.width, image.height),
522         dstOffset: IntOffset = IntOffset.Zero,
523         dstSize: IntSize = srcSize,
524         @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
525         style: DrawStyle = Fill,
526         colorFilter: ColorFilter? = null,
527         blendMode: BlendMode = DefaultBlendMode
528     )
529 
530     /**
531      * Draws the subset of the given image described by the `src` argument into the canvas in the
532      * axis-aligned rectangle given by the `dst` argument.
533      *
534      * If no src rect is provided, the entire image is scaled into the corresponding destination
535      * bounds
536      *
537      * @param image The source image to draw
538      * @param srcOffset Optional offset representing the top left offset of the source image to
539      *   draw, this defaults to the origin of [image]
540      * @param srcSize Optional dimensions of the source image to draw relative to [srcOffset], this
541      *   defaults the width and height of [image]
542      * @param dstOffset Optional offset representing the top left offset of the destination to draw
543      *   the given image, this defaults to the origin of the current translation tarting top left
544      *   offset in the destination to draw the image
545      * @param dstSize Optional dimensions of the destination to draw, this defaults to [srcSize]
546      * @param alpha Opacity to be applied to [image] from 0.0f to 1.0f representing fully
547      *   transparent to fully opaque respectively
548      * @param style Specifies whether the image is to be drawn filled in or as a rectangular stroke
549      * @param colorFilter ColorFilter to apply to the [image] when drawn into the destination
550      * @param blendMode Blending algorithm to apply to destination
551      * @param filterQuality Sampling algorithm applied to the [image] when it is scaled and drawn
552      *   into the destination. The default is [FilterQuality.Low] which scales using a bilinear
553      *   sampling algorithm
554      */
555     fun drawImage(
556         image: ImageBitmap,
557         srcOffset: IntOffset = IntOffset.Zero,
558         srcSize: IntSize = IntSize(image.width, image.height),
559         dstOffset: IntOffset = IntOffset.Zero,
560         dstSize: IntSize = srcSize,
561         @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
562         style: DrawStyle = Fill,
563         colorFilter: ColorFilter? = null,
564         blendMode: BlendMode = DefaultBlendMode,
565         filterQuality: FilterQuality = DefaultFilterQuality
566     ) {
567         drawImage(
568             image = image,
569             srcOffset = srcOffset,
570             srcSize = srcSize,
571             dstOffset = dstOffset,
572             dstSize = dstSize,
573             alpha = alpha,
574             style = style,
575             colorFilter = colorFilter,
576             blendMode = blendMode
577         )
578     }
579 
580     /**
581      * Draws a rounded rectangle with the provided size, offset and radii for the x and y axis
582      * respectively. This rectangle is drawn with the provided [Brush] parameter and is filled or
583      * stroked based on the given [DrawStyle]
584      *
585      * @param brush The color or fill to be applied to the rounded rectangle
586      * @param topLeft Offset from the local origin of 0, 0 relative to the current translation
587      * @param size Dimensions of the rectangle to draw
588      * @param cornerRadius Corner radius of the rounded rectangle, negative radii values are clamped
589      *   to 0
590      * @param alpha Opacity to be applied to rounded rectangle from 0.0f to 1.0f representing fully
591      *   transparent to fully opaque respectively
592      * @param style Specifies whether the rounded rectangle is stroked or filled in
593      * @param colorFilter ColorFilter to apply to the [brush] when drawn into the destination
594      * @param blendMode Blending algorithm to be applied to the brush
595      */
drawRoundRectnull596     fun drawRoundRect(
597         brush: Brush,
598         topLeft: Offset = Offset.Zero,
599         size: Size = this.size.offsetSize(topLeft),
600         cornerRadius: CornerRadius = CornerRadius.Zero,
601         @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
602         style: DrawStyle = Fill,
603         colorFilter: ColorFilter? = null,
604         blendMode: BlendMode = DefaultBlendMode
605     )
606 
607     /**
608      * Draws a rounded rectangle with the given [Paint]. Whether the rectangle is filled or stroked
609      * (or both) is controlled by [Paint.style].
610      *
611      * @param color The color to be applied to the rounded rectangle
612      * @param topLeft Offset from the local origin of 0, 0 relative to the current translation
613      * @param size Dimensions of the rectangle to draw
614      * @param cornerRadius Corner radius of the rounded rectangle, negative radii values are clamped
615      *   to 0
616      * @param alpha Opacity to be applied to rounded rectangle from 0.0f to 1.0f representing fully
617      *   transparent to fully opaque respectively
618      * @param style Specifies whether the rounded rectangle is stroked or filled in
619      * @param colorFilter ColorFilter to apply to the [color] when drawn into the destination
620      * @param blendMode Blending algorithm to be applied to the color
621      */
622     fun drawRoundRect(
623         color: Color,
624         topLeft: Offset = Offset.Zero,
625         size: Size = this.size.offsetSize(topLeft),
626         cornerRadius: CornerRadius = CornerRadius.Zero,
627         style: DrawStyle = Fill,
628         @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
629         colorFilter: ColorFilter? = null,
630         blendMode: BlendMode = DefaultBlendMode
631     )
632 
633     /**
634      * Draws a circle at the provided center coordinate and radius. If no center point is provided
635      * the center of the bounds is used.
636      *
637      * @param brush The color or fill to be applied to the circle
638      * @param radius The radius of the circle
639      * @param center The center coordinate where the circle is to be drawn
640      * @param alpha Opacity to be applied to the circle from 0.0f to 1.0f representing fully
641      *   transparent to fully opaque respectively
642      * @param style Whether or not the circle is stroked or filled in
643      * @param colorFilter ColorFilter to apply to the [brush] when drawn into the destination
644      * @param blendMode Blending algorithm to be applied to the brush
645      */
646     fun drawCircle(
647         brush: Brush,
648         radius: Float = size.minDimension / 2.0f,
649         center: Offset = this.center,
650         @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
651         style: DrawStyle = Fill,
652         colorFilter: ColorFilter? = null,
653         blendMode: BlendMode = DefaultBlendMode
654     )
655 
656     /**
657      * Draws a circle at the provided center coordinate and radius. If no center point is provided
658      * the center of the bounds is used.
659      *
660      * @param color The color or fill to be applied to the circle
661      * @param radius The radius of the circle
662      * @param center The center coordinate where the circle is to be drawn
663      * @param alpha Opacity to be applied to the circle from 0.0f to 1.0f representing fully
664      *   transparent to fully opaque respectively
665      * @param style Whether or not the circle is stroked or filled in
666      * @param colorFilter ColorFilter to apply to the [color] when drawn into the destination
667      * @param blendMode Blending algorithm to be applied to the brush
668      */
669     fun drawCircle(
670         color: Color,
671         radius: Float = size.minDimension / 2.0f,
672         center: Offset = this.center,
673         @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
674         style: DrawStyle = Fill,
675         colorFilter: ColorFilter? = null,
676         blendMode: BlendMode = DefaultBlendMode
677     )
678 
679     /**
680      * Draws an oval with the given offset and size. If no offset from the top left is provided, it
681      * is drawn starting from the origin of the current translation. If no size is provided, the
682      * size of the current environment is used.
683      *
684      * @param brush Color or fill to be applied to the oval
685      * @param topLeft Offset from the local origin of 0, 0 relative to the current translation
686      * @param size Dimensions of the rectangle to draw
687      * @param alpha Opacity to be applied to the oval from 0.0f to 1.0f representing fully
688      *   transparent to fully opaque respectively
689      * @param style Whether or not the oval is stroked or filled in
690      * @param colorFilter ColorFilter to apply to the [brush] when drawn into the destination
691      * @param blendMode Blending algorithm to be applied to the brush
692      * @sample androidx.compose.ui.graphics.samples.DrawScopeOvalBrushSample
693      */
694     fun drawOval(
695         brush: Brush,
696         topLeft: Offset = Offset.Zero,
697         size: Size = this.size.offsetSize(topLeft),
698         @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
699         style: DrawStyle = Fill,
700         colorFilter: ColorFilter? = null,
701         blendMode: BlendMode = DefaultBlendMode
702     )
703 
704     /**
705      * Draws an oval with the given offset and size. If no offset from the top left is provided, it
706      * is drawn starting from the origin of the current translation. If no size is provided, the
707      * size of the current environment is used.
708      *
709      * @param color Color to be applied to the oval
710      * @param topLeft Offset from the local origin of 0, 0 relative to the current translation
711      * @param size Dimensions of the rectangle to draw
712      * @param alpha Opacity to be applied to the oval from 0.0f to 1.0f representing fully
713      *   transparent to fully opaque respectively
714      * @param style Whether or not the oval is stroked or filled in
715      * @param colorFilter ColorFilter to apply to the [color] when drawn into the destination
716      * @param blendMode Blending algorithm to be applied to the brush
717      * @sample androidx.compose.ui.graphics.samples.DrawScopeOvalColorSample
718      */
719     fun drawOval(
720         color: Color,
721         topLeft: Offset = Offset.Zero,
722         size: Size = this.size.offsetSize(topLeft),
723         @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
724         style: DrawStyle = Fill,
725         colorFilter: ColorFilter? = null,
726         blendMode: BlendMode = DefaultBlendMode
727     )
728 
729     /**
730      * Draw an arc scaled to fit inside the given rectangle. It starts from startAngle degrees
731      * around the oval up to startAngle + sweepAngle degrees around the oval, with zero degrees
732      * being the point on the right hand side of the oval that crosses the horizontal line that
733      * intersects the center of the rectangle and with positive angles going clockwise around the
734      * oval. If useCenter is true, the arc is closed back to the center, forming a circle sector.
735      * Otherwise, the arc is not closed, forming a circle segment.
736      *
737      * @param brush Color or fill to be applied to the arc
738      * @param topLeft Offset from the local origin of 0, 0 relative to the current translation
739      * @param size Dimensions of the arc to draw
740      * @param startAngle Starting angle in degrees. 0 represents 3 o'clock
741      * @param sweepAngle Size of the arc in degrees that is drawn clockwise relative to [startAngle]
742      * @param useCenter Flag indicating if the arc is to close the center of the bounds
743      * @param alpha Opacity to be applied to the arc from 0.0f to 1.0f representing fully
744      *   transparent to fully opaque respectively
745      * @param style Whether or not the arc is stroked or filled in
746      * @param colorFilter ColorFilter to apply to the [brush] when drawn into the destination
747      * @param blendMode Blending algorithm to be applied to the arc when it is drawn
748      */
749     fun drawArc(
750         brush: Brush,
751         startAngle: Float,
752         sweepAngle: Float,
753         useCenter: Boolean,
754         topLeft: Offset = Offset.Zero,
755         size: Size = this.size.offsetSize(topLeft),
756         @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
757         style: DrawStyle = Fill,
758         colorFilter: ColorFilter? = null,
759         blendMode: BlendMode = DefaultBlendMode
760     )
761 
762     /**
763      * Draw an arc scaled to fit inside the given rectangle. It starts from startAngle degrees
764      * around the oval up to startAngle + sweepAngle degrees around the oval, with zero degrees
765      * being the point on the right hand side of the oval that crosses the horizontal line that
766      * intersects the center of the rectangle and with positive angles going clockwise around the
767      * oval. If useCenter is true, the arc is closed back to the center, forming a circle sector.
768      * Otherwise, the arc is not closed, forming a circle segment.
769      *
770      * @param color Color to be applied to the arc
771      * @param topLeft Offset from the local origin of 0, 0 relative to the current translation
772      * @param size Dimensions of the arc to draw
773      * @param startAngle Starting angle in degrees. 0 represents 3 o'clock
774      * @param sweepAngle Size of the arc in degrees that is drawn clockwise relative to [startAngle]
775      * @param useCenter Flag indicating if the arc is to close the center of the bounds
776      * @param alpha Opacity to be applied to the arc from 0.0f to 1.0f representing fully
777      *   transparent to fully opaque respectively
778      * @param style Whether or not the arc is stroked or filled in
779      * @param colorFilter ColorFilter to apply to the [color] when drawn into the destination
780      * @param blendMode Blending algorithm to be applied to the arc when it is drawn
781      */
782     fun drawArc(
783         color: Color,
784         startAngle: Float,
785         sweepAngle: Float,
786         useCenter: Boolean,
787         topLeft: Offset = Offset.Zero,
788         size: Size = this.size.offsetSize(topLeft),
789         @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
790         style: DrawStyle = Fill,
791         colorFilter: ColorFilter? = null,
792         blendMode: BlendMode = DefaultBlendMode
793     )
794 
795     /**
796      * Draws the given [Path] with the given [Color]. Whether this shape is filled or stroked (or
797      * both) is controlled by [DrawStyle]. If the path is filled, then subpaths within it are
798      * implicitly closed (see [Path.close]).
799      *
800      * @param path Path to draw
801      * @param color Color to be applied to the path
802      * @param alpha Opacity to be applied to the path from 0.0f to 1.0f representing fully
803      *   transparent to fully opaque respectively
804      * @param style Whether or not the path is stroked or filled in
805      * @param colorFilter ColorFilter to apply to the [color] when drawn into the destination
806      * @param blendMode Blending algorithm to be applied to the path when it is drawn
807      */
808     fun drawPath(
809         path: Path,
810         color: Color,
811         @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
812         style: DrawStyle = Fill,
813         colorFilter: ColorFilter? = null,
814         blendMode: BlendMode = DefaultBlendMode
815     )
816 
817     /**
818      * Draws the given [Path] with the given [Color]. Whether this shape is filled or stroked (or
819      * both) is controlled by [DrawStyle]. If the path is filled, then subpaths within it are
820      * implicitly closed (see [Path.close]).
821      *
822      * @param path Path to draw
823      * @param brush Brush to be applied to the path
824      * @param alpha Opacity to be applied to the path from 0.0f to 1.0f representing fully
825      *   transparent to fully opaque respectively
826      * @param style Whether or not the path is stroked or filled in
827      * @param colorFilter ColorFilter to apply to the [brush] when drawn into the destination
828      * @param blendMode Blending algorithm to be applied to the path when it is drawn
829      */
830     fun drawPath(
831         path: Path,
832         brush: Brush,
833         @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
834         style: DrawStyle = Fill,
835         colorFilter: ColorFilter? = null,
836         blendMode: BlendMode = DefaultBlendMode
837     )
838 
839     /**
840      * Draws a sequence of points according to the given [PointMode].
841      *
842      * The `points` argument is interpreted as offsets from the origin.
843      *
844      * @param points List of points to draw with the specified [PointMode]
845      * @param pointMode [PointMode] used to indicate how the points are to be drawn
846      * @param color Color to be applied to the points
847      * @param alpha Opacity to be applied to the path from 0.0f to 1.0f representing fully
848      *   transparent to fully opaque respectively
849      * @param strokeWidth The stroke width to apply to the line
850      * @param cap Treatment applied to the ends of the line segment
851      * @param pathEffect optional effect or pattern to apply to the point
852      * @param colorFilter ColorFilter to apply to the [color] when drawn into the destination
853      * @param blendMode Blending algorithm to be applied to the path when it is drawn
854      */
855     fun drawPoints(
856         points: List<Offset>,
857         pointMode: PointMode,
858         color: Color,
859         strokeWidth: Float = Stroke.HairlineWidth,
860         cap: StrokeCap = StrokeCap.Butt,
861         pathEffect: PathEffect? = null,
862         @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
863         colorFilter: ColorFilter? = null,
864         blendMode: BlendMode = DefaultBlendMode
865     )
866 
867     /**
868      * Draws a sequence of points according to the given [PointMode].
869      *
870      * The `points` argument is interpreted as offsets from the origin.
871      *
872      * @param points List of points to draw with the specified [PointMode]
873      * @param pointMode [PointMode] used to indicate how the points are to be drawn
874      * @param brush Brush to be applied to the points
875      * @param strokeWidth The stroke width to apply to the line
876      * @param cap Treatment applied to the ends of the line segment
877      * @param pathEffect optional effect or pattern to apply to the points
878      * @param alpha Opacity to be applied to the path from 0.0f to 1.0f representing fully
879      *   transparent to fully opaque respectively.
880      * @param colorFilter ColorFilter to apply to the [brush] when drawn into the destination
881      * @param blendMode Blending algorithm to be applied to the path when it is drawn
882      */
883     fun drawPoints(
884         points: List<Offset>,
885         pointMode: PointMode,
886         brush: Brush,
887         strokeWidth: Float = Stroke.HairlineWidth,
888         cap: StrokeCap = StrokeCap.Butt,
889         pathEffect: PathEffect? = null,
890         @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
891         colorFilter: ColorFilter? = null,
892         blendMode: BlendMode = DefaultBlendMode
893     )
894 
895     /**
896      * Record the corresponding drawing commands for this [GraphicsLayer] instance using the
897      * [Density], [LayoutDirection] and [IntSize] from the provided [DrawScope] as defaults. This
898      * will retarget the underlying canvas of the provided DrawScope to draw within the layer itself
899      * and reset it to the original canvas on the conclusion of this method call.
900      */
901     fun GraphicsLayer.record(
902         size: IntSize = this@DrawScope.size.toIntSize(),
903         block: DrawScope.() -> Unit
904     ) =
905         record(this@DrawScope, this@DrawScope.layoutDirection, size) {
906             this@DrawScope.draw(
907                 // we can use this@record.drawContext directly as the values in this@DrawScope
908                 // and this@record are the same
909                 drawContext.density,
910                 drawContext.layoutDirection,
911                 drawContext.canvas,
912                 drawContext.size,
913                 drawContext.graphicsLayer,
914                 block
915             )
916         }
917 
918     /** Helper method to offset the provided size with the offset in box width and height */
offsetSizenull919     private fun Size.offsetSize(offset: Offset): Size =
920         Size(this.width - offset.x, this.height - offset.y)
921 
922     companion object {
923 
924         /**
925          * Default blending mode used for each drawing operation. This ensures that content is drawn
926          * on top of the pixels in the destination
927          */
928         val DefaultBlendMode: BlendMode = BlendMode.SrcOver
929 
930         /**
931          * Default FilterQuality used for determining the filtering algorithm to apply when scaling
932          * [ImageBitmap] objects. Maps to the default behavior of bilinear filtering
933          */
934         val DefaultFilterQuality: FilterQuality = FilterQuality.Low
935     }
936 }
937 
938 /** Represents how the shapes should be drawn within a [DrawScope] */
939 sealed class DrawStyle
940 
941 /**
942  * Default [DrawStyle] indicating shapes should be drawn completely filled in with the provided
943  * color or pattern
944  */
945 object Fill : DrawStyle()
946 
947 /**
948  * [DrawStyle] that provides information for drawing content with a stroke
949  *
950  * @param width Configure the width of the stroke in pixels
951  * @param miter Set the stroke miter value. This is used to control the behavior of miter joins when
952  *   the joins angle is sharp. This value must be >= 0
953  * @param cap Return the paint's Cap, controlling how the start and end of stroked lines and paths
954  *   are treated. The default is [StrokeCap.Butt]
955  * @param join Set's the treatment where lines and curve segments join on a stroked path. The
956  *   default is [StrokeJoin.Miter]
957  * @param pathEffect Effect to apply to the stroke, null indicates a solid stroke line is to be
958  *   drawn
959  */
960 class Stroke(
961     val width: Float = 0.0f,
962     val miter: Float = DefaultMiter,
963     val cap: StrokeCap = DefaultCap,
964     val join: StrokeJoin = DefaultJoin,
965     val pathEffect: PathEffect? = null
966 ) : DrawStyle() {
967     companion object {
968 
969         /** Width to indicate a hairline stroke of 1 pixel */
970         const val HairlineWidth = 0.0f
971 
972         /** Default miter length used in combination with joins */
973         const val DefaultMiter: Float = 4.0f
974 
975         /** Default cap used for line endings */
976         val DefaultCap = StrokeCap.Butt
977 
978         /** Default join style used for connections between line and curve segments */
979         val DefaultJoin = StrokeJoin.Miter
980     }
981 
equalsnull982     override fun equals(other: Any?): Boolean {
983         if (this === other) return true
984         if (other !is Stroke) return false
985 
986         if (width != other.width) return false
987         if (miter != other.miter) return false
988         if (cap != other.cap) return false
989         if (join != other.join) return false
990         if (pathEffect != other.pathEffect) return false
991 
992         return true
993     }
994 
hashCodenull995     override fun hashCode(): Int {
996         var result = width.hashCode()
997         result = 31 * result + miter.hashCode()
998         result = 31 * result + cap.hashCode()
999         result = 31 * result + join.hashCode()
1000         result = 31 * result + (pathEffect?.hashCode() ?: 0)
1001         return result
1002     }
1003 
toStringnull1004     override fun toString(): String {
1005         return "Stroke(width=$width, miter=$miter, cap=$cap, join=$join, pathEffect=$pathEffect)"
1006     }
1007 }
1008