1 /*
2 * Copyright 2019 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.vector
18
19 import androidx.compose.runtime.Immutable
20 import androidx.compose.ui.graphics.BlendMode
21 import androidx.compose.ui.graphics.Brush
22 import androidx.compose.ui.graphics.Color
23 import androidx.compose.ui.graphics.Path
24 import androidx.compose.ui.graphics.PathFillType
25 import androidx.compose.ui.graphics.StrokeCap
26 import androidx.compose.ui.graphics.StrokeJoin
27 import androidx.compose.ui.internal.checkPrecondition
28 import androidx.compose.ui.platform.makeSynchronizedObject
29 import androidx.compose.ui.platform.synchronized
30 import androidx.compose.ui.unit.Dp
31
32 /**
33 * Vector graphics object that is generated as a result of [ImageVector.Builder] It can be composed
34 * and rendered by passing it as an argument to [rememberVectorPainter]
35 */
36 @Immutable
37 class ImageVector
38 internal constructor(
39
40 /** Name of the Vector asset */
41 val name: String,
42
43 /** Intrinsic width of the vector asset in [Dp] */
44 val defaultWidth: Dp,
45
46 /** Intrinsic height of the vector asset in [Dp] */
47 val defaultHeight: Dp,
48
49 /**
50 * Used to define the width of the viewport space. Viewport is basically the virtual canvas
51 * where the paths are drawn on.
52 */
53 val viewportWidth: Float,
54
55 /**
56 * Used to define the height of the viewport space. Viewport is basically the virtual canvas
57 * where the paths are drawn on.
58 */
59 val viewportHeight: Float,
60
61 /** Root group of the vector asset that contains all the child groups and paths */
62 val root: VectorGroup,
63
64 /** Optional tint color to be applied to the vector graphic */
65 val tintColor: Color,
66
67 /** Blend mode used to apply [tintColor] */
68 val tintBlendMode: BlendMode,
69
70 /** Determines if the vector asset should automatically be mirrored for right to left locales */
71 val autoMirror: Boolean,
72
73 /**
74 * Identifier used to disambiguate between different ImageVector instances in a more efficient
75 * manner than equality. This can be used as a key for caching instances of ImageVectors.
76 */
77 internal val genId: Int = generateImageVectorId(),
78 ) {
79 /**
80 * Builder used to construct a Vector graphic tree. This is useful for caching the result of
81 * expensive operations used to construct a vector graphic for compose. For example, the vector
82 * graphic could be serialized and downloaded from a server and represented internally in a
83 * ImageVector before it is composed through [rememberVectorPainter] The generated ImageVector
84 * is recommended to be memoized across composition calls to avoid doing redundant work
85 */
86 @Suppress("MissingGetterMatchingBuilder")
87 class Builder(
88
89 /** Name of the vector asset */
90 private val name: String = DefaultGroupName,
91
92 /** Intrinsic width of the Vector in [Dp] */
93 private val defaultWidth: Dp,
94
95 /** Intrinsic height of the Vector in [Dp] */
96 private val defaultHeight: Dp,
97
98 /**
99 * Used to define the width of the viewport space. Viewport is basically the virtual canvas
100 * where the paths are drawn on.
101 */
102 private val viewportWidth: Float,
103
104 /**
105 * Used to define the height of the viewport space. Viewport is basically the virtual canvas
106 * where the paths are drawn on.
107 */
108 private val viewportHeight: Float,
109
110 /** Optional color used to tint the entire vector image */
111 private val tintColor: Color = Color.Unspecified,
112
113 /** Blend mode used to apply the tint color */
114 private val tintBlendMode: BlendMode = BlendMode.SrcIn,
115
116 /**
117 * Determines if the vector asset should automatically be mirrored for right to left locales
118 */
119 private val autoMirror: Boolean = false
120 ) {
121
122 // Secondary constructor to maintain API compatibility that defaults autoMirror to false
123 @Deprecated(
124 "Replace with ImageVector.Builder that consumes an optional auto " + "mirror parameter",
125 replaceWith =
126 ReplaceWith(
127 "Builder(name, defaultWidth, defaultHeight, viewportWidth, " +
128 "viewportHeight, tintColor, tintBlendMode, false)",
129 "androidx.compose.ui.graphics.vector"
130 ),
131 DeprecationLevel.HIDDEN
132 )
133 constructor(
134 /** Name of the vector asset */
135 name: String = DefaultGroupName,
136
137 /** Intrinsic width of the Vector in [Dp] */
138 defaultWidth: Dp,
139
140 /** Intrinsic height of the Vector in [Dp] */
141 defaultHeight: Dp,
142
143 /**
144 * Used to define the width of the viewport space. Viewport is basically the virtual
145 * canvas where the paths are drawn on.
146 */
147 viewportWidth: Float,
148
149 /**
150 * Used to define the height of the viewport space. Viewport is basically the virtual
151 * canvas where the paths are drawn on.
152 */
153 viewportHeight: Float,
154
155 /** Optional color used to tint the entire vector image */
156 tintColor: Color = Color.Unspecified,
157
158 /** Blend mode used to apply the tint color */
159 tintBlendMode: BlendMode = BlendMode.SrcIn
160 ) : this(
161 name,
162 defaultWidth,
163 defaultHeight,
164 viewportWidth,
165 viewportHeight,
166 tintColor,
167 tintBlendMode,
168 false
169 )
170
171 private val nodes = ArrayList<GroupParams>()
172
173 private var root = GroupParams()
174 private var isConsumed = false
175
176 private val currentGroup: GroupParams
177 get() = nodes.peek()
178
179 init {
180 nodes.push(root)
181 }
182
183 /**
184 * Create a new group and push it to the front of the stack of ImageVector nodes
185 *
186 * @param name the name of the group
187 * @param rotate the rotation of the group in degrees
188 * @param pivotX the x coordinate of the pivot point to rotate or scale the group
189 * @param pivotY the y coordinate of the pivot point to rotate or scale the group
190 * @param scaleX the scale factor in the X-axis to apply to the group
191 * @param scaleY the scale factor in the Y-axis to apply to the group
192 * @param translationX the translation in virtual pixels to apply along the x-axis
193 * @param translationY the translation in virtual pixels to apply along the y-axis
194 * @param clipPathData the path information used to clip the content within the group
195 * @return This ImageVector.Builder instance as a convenience for chaining calls
196 */
197 @Suppress("MissingGetterMatchingBuilder")
addGroupnull198 fun addGroup(
199 name: String = DefaultGroupName,
200 rotate: Float = DefaultRotation,
201 pivotX: Float = DefaultPivotX,
202 pivotY: Float = DefaultPivotY,
203 scaleX: Float = DefaultScaleX,
204 scaleY: Float = DefaultScaleY,
205 translationX: Float = DefaultTranslationX,
206 translationY: Float = DefaultTranslationY,
207 clipPathData: List<PathNode> = EmptyPath
208 ): Builder {
209 ensureNotConsumed()
210 val group =
211 GroupParams(
212 name,
213 rotate,
214 pivotX,
215 pivotY,
216 scaleX,
217 scaleY,
218 translationX,
219 translationY,
220 clipPathData
221 )
222 nodes.push(group)
223 return this
224 }
225
226 /**
227 * Pops the topmost VectorGroup from this ImageVector.Builder. This is used to indicate that
228 * no additional ImageVector nodes will be added to the current VectorGroup
229 *
230 * @return This ImageVector.Builder instance as a convenience for chaining calls
231 */
clearGroupnull232 fun clearGroup(): Builder {
233 ensureNotConsumed()
234 val popped = nodes.pop()
235 currentGroup.children.add(popped.asVectorGroup())
236 return this
237 }
238
239 /**
240 * Add a path to the ImageVector graphic. This represents a leaf node in the ImageVector
241 * graphics tree structure
242 *
243 * @param pathData path information to render the shape of the path
244 * @param pathFillType rule to determine how the interior of the path is to be calculated
245 * @param name the name of the path
246 * @param fill specifies the [Brush] used to fill the path
247 * @param fillAlpha the alpha to fill the path
248 * @param stroke specifies the [Brush] used to fill the stroke
249 * @param strokeAlpha the alpha to stroke the path
250 * @param strokeLineWidth the width of the line to stroke the path
251 * @param strokeLineCap specifies the linecap for a stroked path
252 * @param strokeLineJoin specifies the linejoin for a stroked path
253 * @param strokeLineMiter specifies the miter limit for a stroked path
254 * @param trimPathStart specifies the fraction of the path to trim from the start in the
255 * range from 0 to 1. Values outside the range will wrap around the length of the path.
256 * Default is 0.
257 * @param trimPathEnd specifies the fraction of the path to trim from the end in the range
258 * from 0 to 1. Values outside the range will wrap around the length of the path. Default
259 * is 1.
260 * @param trimPathOffset specifies the fraction to shift the path trim region in the range
261 * from 0 to 1. Values outside the range will wrap around the length of the path. Default
262 * is 0.
263 * @return This ImageVector.Builder instance as a convenience for chaining calls
264 */
265 @Suppress("MissingGetterMatchingBuilder")
addPathnull266 fun addPath(
267 pathData: List<PathNode>,
268 pathFillType: PathFillType = DefaultFillType,
269 name: String = DefaultPathName,
270 fill: Brush? = null,
271 fillAlpha: Float = 1.0f,
272 stroke: Brush? = null,
273 strokeAlpha: Float = 1.0f,
274 strokeLineWidth: Float = DefaultStrokeLineWidth,
275 strokeLineCap: StrokeCap = DefaultStrokeLineCap,
276 strokeLineJoin: StrokeJoin = DefaultStrokeLineJoin,
277 strokeLineMiter: Float = DefaultStrokeLineMiter,
278 trimPathStart: Float = DefaultTrimPathStart,
279 trimPathEnd: Float = DefaultTrimPathEnd,
280 trimPathOffset: Float = DefaultTrimPathOffset
281 ): Builder {
282 ensureNotConsumed()
283 currentGroup.children.add(
284 VectorPath(
285 name,
286 pathData,
287 pathFillType,
288 fill,
289 fillAlpha,
290 stroke,
291 strokeAlpha,
292 strokeLineWidth,
293 strokeLineCap,
294 strokeLineJoin,
295 strokeLineMiter,
296 trimPathStart,
297 trimPathEnd,
298 trimPathOffset
299 )
300 )
301 return this
302 }
303
304 /**
305 * Construct a ImageVector. This concludes the creation process of a ImageVector graphic
306 * This builder cannot be re-used to create additional ImageVector instances
307 *
308 * @return The newly created ImageVector instance
309 */
buildnull310 fun build(): ImageVector {
311 ensureNotConsumed()
312 // pop all groups except for the root
313 while (nodes.size > 1) {
314 clearGroup()
315 }
316
317 val vectorImage =
318 ImageVector(
319 name,
320 defaultWidth,
321 defaultHeight,
322 viewportWidth,
323 viewportHeight,
324 root.asVectorGroup(),
325 tintColor,
326 tintBlendMode,
327 autoMirror
328 )
329
330 isConsumed = true
331
332 return vectorImage
333 }
334
335 /** Throws IllegalStateException if the ImageVector.Builder has already been consumed */
ensureNotConsumednull336 private fun ensureNotConsumed() {
337 checkPrecondition(!isConsumed) {
338 "ImageVector.Builder is single use, create a new instance " +
339 "to create a new ImageVector"
340 }
341 }
342
343 /**
344 * Helper method to create an immutable VectorGroup object from an set of GroupParams which
345 * represent a group that is in the middle of being constructed
346 */
asVectorGroupnull347 private fun GroupParams.asVectorGroup(): VectorGroup =
348 VectorGroup(
349 name,
350 rotate,
351 pivotX,
352 pivotY,
353 scaleX,
354 scaleY,
355 translationX,
356 translationY,
357 clipPathData,
358 children
359 )
360
361 /**
362 * Internal helper class to help assist with in progress creation of a vector group before
363 * creating the immutable result
364 */
365 private class GroupParams(
366 var name: String = DefaultGroupName,
367 var rotate: Float = DefaultRotation,
368 var pivotX: Float = DefaultPivotX,
369 var pivotY: Float = DefaultPivotY,
370 var scaleX: Float = DefaultScaleX,
371 var scaleY: Float = DefaultScaleY,
372 var translationX: Float = DefaultTranslationX,
373 var translationY: Float = DefaultTranslationY,
374 var clipPathData: List<PathNode> = EmptyPath,
375 var children: MutableList<VectorNode> = mutableListOf()
376 )
377 }
378
379 companion object {
380 private var imageVectorCount = 0
381 private val lock = makeSynchronizedObject(this)
382
383 internal fun generateImageVectorId(): Int {
384 synchronized(lock) {
385 return imageVectorCount++
386 }
387 }
388 }
389
equalsnull390 override fun equals(other: Any?): Boolean {
391 if (this === other) return true
392 if (other !is ImageVector) return false
393
394 if (name != other.name) return false
395 if (defaultWidth != other.defaultWidth) return false
396 if (defaultHeight != other.defaultHeight) return false
397 if (viewportWidth != other.viewportWidth) return false
398 if (viewportHeight != other.viewportHeight) return false
399 if (root != other.root) return false
400 if (tintColor != other.tintColor) return false
401 if (tintBlendMode != other.tintBlendMode) return false
402 if (autoMirror != other.autoMirror) return false
403 return true
404 }
405
hashCodenull406 override fun hashCode(): Int {
407 var result = name.hashCode()
408 result = 31 * result + defaultWidth.hashCode()
409 result = 31 * result + defaultHeight.hashCode()
410 result = 31 * result + viewportWidth.hashCode()
411 result = 31 * result + viewportHeight.hashCode()
412 result = 31 * result + root.hashCode()
413 result = 31 * result + tintColor.hashCode()
414 result = 31 * result + tintBlendMode.hashCode()
415 result = 31 * result + autoMirror.hashCode()
416 return result
417 }
418 }
419
420 sealed class VectorNode
421
422 /**
423 * Defines a group of paths or subgroups, plus transformation information. The transformations are
424 * defined in the same coordinates as the viewport. The transformations are applied in the order of
425 * scale, rotate then translate.
426 *
427 * This is constructed as part of the result of [ImageVector.Builder] construction
428 */
429 @Immutable
430 class VectorGroup
431 internal constructor(
432 /** Name of the corresponding group */
433 val name: String = DefaultGroupName,
434
435 /** Rotation of the group in degrees */
436 val rotation: Float = DefaultRotation,
437
438 /** X coordinate of the pivot point to rotate or scale the group */
439 val pivotX: Float = DefaultPivotX,
440
441 /** Y coordinate of the pivot point to rotate or scale the group */
442 val pivotY: Float = DefaultPivotY,
443
444 /** Scale factor in the X-axis to apply to the group */
445 val scaleX: Float = DefaultScaleX,
446
447 /** Scale factor in the Y-axis to apply to the group */
448 val scaleY: Float = DefaultScaleY,
449
450 /** Translation in virtual pixels to apply along the x-axis */
451 val translationX: Float = DefaultTranslationX,
452
453 /** Translation in virtual pixels to apply along the y-axis */
454 val translationY: Float = DefaultTranslationY,
455
456 /** Path information used to clip the content within the group */
457 val clipPathData: List<PathNode> = EmptyPath,
458
459 /** Child Vector nodes that are part of this group, this can contain paths or other groups */
460 private val children: List<VectorNode> = emptyList()
461 ) : VectorNode(), Iterable<VectorNode> {
462
463 val size: Int
464 get() = children.size
465
getnull466 operator fun get(index: Int): VectorNode {
467 return children[index]
468 }
469
iteratornull470 override fun iterator(): Iterator<VectorNode> {
471 return object : Iterator<VectorNode> {
472
473 val it = children.iterator()
474
475 override fun hasNext(): Boolean = it.hasNext()
476
477 override fun next(): VectorNode = it.next()
478 }
479 }
480
equalsnull481 override fun equals(other: Any?): Boolean {
482 if (this === other) return true
483 if (other == null || other !is VectorGroup) return false
484
485 if (name != other.name) return false
486 if (rotation != other.rotation) return false
487 if (pivotX != other.pivotX) return false
488 if (pivotY != other.pivotY) return false
489 if (scaleX != other.scaleX) return false
490 if (scaleY != other.scaleY) return false
491 if (translationX != other.translationX) return false
492 if (translationY != other.translationY) return false
493 if (clipPathData != other.clipPathData) return false
494 if (children != other.children) return false
495
496 return true
497 }
498
hashCodenull499 override fun hashCode(): Int {
500 var result = name.hashCode()
501 result = 31 * result + rotation.hashCode()
502 result = 31 * result + pivotX.hashCode()
503 result = 31 * result + pivotY.hashCode()
504 result = 31 * result + scaleX.hashCode()
505 result = 31 * result + scaleY.hashCode()
506 result = 31 * result + translationX.hashCode()
507 result = 31 * result + translationY.hashCode()
508 result = 31 * result + clipPathData.hashCode()
509 result = 31 * result + children.hashCode()
510 return result
511 }
512 }
513
514 /**
515 * Leaf node of a Vector graphics tree. This specifies a path shape and parameters to color and
516 * style the shape itself
517 *
518 * This is constructed as part of the result of [ImageVector.Builder] construction
519 */
520 @Immutable
521 class VectorPath
522 internal constructor(
523 /** Name of the corresponding path */
524 val name: String = DefaultPathName,
525
526 /** Path information to render the shape of the path */
527 val pathData: List<PathNode>,
528
529 /** Rule to determine how the interior of the path is to be calculated */
530 val pathFillType: PathFillType,
531
532 /** Specifies the color or gradient used to fill the path */
533 val fill: Brush? = null,
534
535 /** Opacity to fill the path */
536 val fillAlpha: Float = 1.0f,
537
538 /** Specifies the color or gradient used to fill the stroke */
539 val stroke: Brush? = null,
540
541 /** Opacity to stroke the path */
542 val strokeAlpha: Float = 1.0f,
543
544 /** Width of the line to stroke the path */
545 val strokeLineWidth: Float = DefaultStrokeLineWidth,
546
547 /**
548 * Specifies the linecap for a stroked path, either butt, round, or square. The default is butt.
549 */
550 val strokeLineCap: StrokeCap = DefaultStrokeLineCap,
551
552 /**
553 * Specifies the linejoin for a stroked path, either miter, round or bevel. The default is miter
554 */
555 val strokeLineJoin: StrokeJoin = DefaultStrokeLineJoin,
556
557 /** Specifies the miter limit for a stroked path, the default is 4 */
558 val strokeLineMiter: Float = DefaultStrokeLineMiter,
559
560 /**
561 * Specifies the fraction of the path to trim from the start, in the range from 0 to 1. The
562 * default is 0.
563 */
564 val trimPathStart: Float = DefaultTrimPathStart,
565
566 /**
567 * Specifies the fraction of the path to trim from the end, in the range from 0 to 1. The
568 * default is 1.
569 */
570 val trimPathEnd: Float = DefaultTrimPathEnd,
571
572 /**
573 * Specifies the offset of the trim region (allows showed region to include the start and end),
574 * in the range from 0 to 1. The default is 0.
575 */
576 val trimPathOffset: Float = DefaultTrimPathOffset
577 ) : VectorNode() {
578
equalsnull579 override fun equals(other: Any?): Boolean {
580 if (this === other) return true
581 if (other == null || this::class != other::class) return false
582
583 other as VectorPath
584
585 if (name != other.name) return false
586 if (fill != other.fill) return false
587 if (fillAlpha != other.fillAlpha) return false
588 if (stroke != other.stroke) return false
589 if (strokeAlpha != other.strokeAlpha) return false
590 if (strokeLineWidth != other.strokeLineWidth) return false
591 if (strokeLineCap != other.strokeLineCap) return false
592 if (strokeLineJoin != other.strokeLineJoin) return false
593 if (strokeLineMiter != other.strokeLineMiter) return false
594 if (trimPathStart != other.trimPathStart) return false
595 if (trimPathEnd != other.trimPathEnd) return false
596 if (trimPathOffset != other.trimPathOffset) return false
597 if (pathFillType != other.pathFillType) return false
598 if (pathData != other.pathData) return false
599
600 return true
601 }
602
hashCodenull603 override fun hashCode(): Int {
604 var result = name.hashCode()
605 result = 31 * result + pathData.hashCode()
606 result = 31 * result + (fill?.hashCode() ?: 0)
607 result = 31 * result + fillAlpha.hashCode()
608 result = 31 * result + (stroke?.hashCode() ?: 0)
609 result = 31 * result + strokeAlpha.hashCode()
610 result = 31 * result + strokeLineWidth.hashCode()
611 result = 31 * result + strokeLineCap.hashCode()
612 result = 31 * result + strokeLineJoin.hashCode()
613 result = 31 * result + strokeLineMiter.hashCode()
614 result = 31 * result + trimPathStart.hashCode()
615 result = 31 * result + trimPathEnd.hashCode()
616 result = 31 * result + trimPathOffset.hashCode()
617 result = 31 * result + pathFillType.hashCode()
618 return result
619 }
620 }
621
622 /**
623 * DSL extension for adding a [VectorPath] to [this].
624 *
625 * See [ImageVector.Builder.addPath] for the corresponding builder function.
626 *
627 * @param name the name for this path
628 * @param fill specifies the [Brush] used to fill the path
629 * @param fillAlpha the alpha to fill the path
630 * @param stroke specifies the [Brush] used to fill the stroke
631 * @param strokeAlpha the alpha to stroke the path
632 * @param strokeLineWidth the width of the line to stroke the path
633 * @param strokeLineCap specifies the linecap for a stroked path
634 * @param strokeLineJoin specifies the linejoin for a stroked path
635 * @param strokeLineMiter specifies the miter limit for a stroked path
636 * @param pathFillType specifies the winding rule that decides how the interior of a [Path] is
637 * calculated.
638 * @param pathBuilder [PathBuilder] lambda for adding [PathNode]s to this path.
639 */
pathnull640 inline fun ImageVector.Builder.path(
641 name: String = DefaultPathName,
642 fill: Brush? = null,
643 fillAlpha: Float = 1.0f,
644 stroke: Brush? = null,
645 strokeAlpha: Float = 1.0f,
646 strokeLineWidth: Float = DefaultStrokeLineWidth,
647 strokeLineCap: StrokeCap = DefaultStrokeLineCap,
648 strokeLineJoin: StrokeJoin = DefaultStrokeLineJoin,
649 strokeLineMiter: Float = DefaultStrokeLineMiter,
650 pathFillType: PathFillType = DefaultFillType,
651 pathBuilder: PathBuilder.() -> Unit
652 ) =
653 addPath(
654 PathData(pathBuilder),
655 pathFillType,
656 name,
657 fill,
658 fillAlpha,
659 stroke,
660 strokeAlpha,
661 strokeLineWidth,
662 strokeLineCap,
663 strokeLineJoin,
664 strokeLineMiter
665 )
666
667 /**
668 * DSL extension for adding a [VectorGroup] to [this].
669 *
670 * See [ImageVector.Builder.pushGroup] for the corresponding builder function.
671 *
672 * @param name the name of the group
673 * @param rotate the rotation of the group in degrees
674 * @param pivotX the x coordinate of the pivot point to rotate or scale the group
675 * @param pivotY the y coordinate of the pivot point to rotate or scale the group
676 * @param scaleX the scale factor in the X-axis to apply to the group
677 * @param scaleY the scale factor in the Y-axis to apply to the group
678 * @param translationX the translation in virtual pixels to apply along the x-axis
679 * @param translationY the translation in virtual pixels to apply along the y-axis
680 * @param clipPathData the path information used to clip the content within the group
681 * @param block builder lambda to add children to this group
682 */
683 inline fun ImageVector.Builder.group(
684 name: String = DefaultGroupName,
685 rotate: Float = DefaultRotation,
686 pivotX: Float = DefaultPivotX,
687 pivotY: Float = DefaultPivotY,
688 scaleX: Float = DefaultScaleX,
689 scaleY: Float = DefaultScaleY,
690 translationX: Float = DefaultTranslationX,
691 translationY: Float = DefaultTranslationY,
692 clipPathData: List<PathNode> = EmptyPath,
693 block: ImageVector.Builder.() -> Unit
694 ) = apply {
695 addGroup(name, rotate, pivotX, pivotY, scaleX, scaleY, translationX, translationY, clipPathData)
696 block()
697 clearGroup()
698 }
699
pushnull700 private fun <T> ArrayList<T>.push(value: T): Boolean = add(value)
701
702 private fun <T> ArrayList<T>.pop(): T = this.removeAt(size - 1)
703
704 private fun <T> ArrayList<T>.peek(): T = this[size - 1]
705