1 /*
<lambda>null2  * Copyright 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.ui.node
18 
19 import androidx.collection.MutableObjectIntMap
20 import androidx.collection.mutableObjectIntMapOf
21 import androidx.compose.runtime.snapshots.Snapshot
22 import androidx.compose.ui.Modifier
23 import androidx.compose.ui.geometry.MutableRect
24 import androidx.compose.ui.geometry.Offset
25 import androidx.compose.ui.geometry.Rect
26 import androidx.compose.ui.geometry.Size
27 import androidx.compose.ui.geometry.isFinite
28 import androidx.compose.ui.geometry.isSpecified
29 import androidx.compose.ui.geometry.toRect
30 import androidx.compose.ui.graphics.Canvas
31 import androidx.compose.ui.graphics.DefaultCameraDistance
32 import androidx.compose.ui.graphics.GraphicsLayerScope
33 import androidx.compose.ui.graphics.Matrix
34 import androidx.compose.ui.graphics.Paint
35 import androidx.compose.ui.graphics.ReusableGraphicsLayerScope
36 import androidx.compose.ui.graphics.TransformOrigin
37 import androidx.compose.ui.graphics.layer.GraphicsLayer
38 import androidx.compose.ui.input.pointer.MatrixPositionCalculator
39 import androidx.compose.ui.input.pointer.PointerType
40 import androidx.compose.ui.internal.checkPrecondition
41 import androidx.compose.ui.internal.checkPreconditionNotNull
42 import androidx.compose.ui.internal.requirePrecondition
43 import androidx.compose.ui.layout.AlignmentLine
44 import androidx.compose.ui.layout.LayoutCoordinates
45 import androidx.compose.ui.layout.LookaheadLayoutCoordinates
46 import androidx.compose.ui.layout.Measurable
47 import androidx.compose.ui.layout.MeasureResult
48 import androidx.compose.ui.layout.Placeable
49 import androidx.compose.ui.layout.findRootCoordinates
50 import androidx.compose.ui.layout.positionInRoot
51 import androidx.compose.ui.layout.positionOnScreen
52 import androidx.compose.ui.ui.FrameRateCategory
53 import androidx.compose.ui.unit.Constraints
54 import androidx.compose.ui.unit.Density
55 import androidx.compose.ui.unit.IntOffset
56 import androidx.compose.ui.unit.IntSize
57 import androidx.compose.ui.unit.LayoutDirection
58 import androidx.compose.ui.unit.minus
59 import androidx.compose.ui.unit.plus
60 import androidx.compose.ui.unit.toSize
61 import androidx.compose.ui.util.fastIsFinite
62 
63 /** Measurable and Placeable type that has a position. */
64 internal abstract class NodeCoordinator(
65     override val layoutNode: LayoutNode,
66 ) : LookaheadCapablePlaceable(), Measurable, LayoutCoordinates, OwnerScope {
67 
68     internal var forcePlaceWithLookaheadOffset: Boolean = false
69     internal var forceMeasureWithLookaheadConstraints: Boolean = false
70     abstract val tail: Modifier.Node
71 
72     internal var wrapped: NodeCoordinator? = null
73     internal var wrappedBy: NodeCoordinator? = null
74 
75     override val layoutDirection: LayoutDirection
76         get() = layoutNode.layoutDirection
77 
78     override val density: Float
79         get() = layoutNode.density.density
80 
81     override val fontScale: Float
82         get() = layoutNode.density.fontScale
83 
84     override val parent: LookaheadCapablePlaceable?
85         get() = wrappedBy
86 
87     override val coordinates: LayoutCoordinates
88         get() = this
89 
90     override val introducesMotionFrameOfReference: Boolean
91         get() = isPlacedUnderMotionFrameOfReference
92 
93     private var released = false
94 
95     private fun headNode(includeTail: Boolean): Modifier.Node? {
96         return if (layoutNode.outerCoordinator === this) {
97             layoutNode.nodes.head
98         } else if (includeTail) {
99             wrappedBy?.tail?.child
100         } else {
101             wrappedBy?.tail
102         }
103     }
104 
105     inline fun visitNodes(mask: Int, includeTail: Boolean, block: (Modifier.Node) -> Unit) {
106         val stopNode = if (includeTail) tail else (tail.parent ?: return)
107         var node: Modifier.Node? = headNode(includeTail)
108         while (node != null) {
109             if (node.aggregateChildKindSet and mask == 0) return
110             if (node.kindSet and mask != 0) block(node)
111             if (node === stopNode) break
112             node = node.child
113         }
114     }
115 
116     inline fun <reified T> visitNodes(type: NodeKind<T>, block: (T) -> Unit) {
117         visitNodes(type.mask, type.includeSelfInTraversal) { it.dispatchForKind(type, block) }
118     }
119 
120     private fun hasNode(type: NodeKind<*>): Boolean {
121         return headNode(type.includeSelfInTraversal)?.has(type) == true
122     }
123 
124     fun head(type: NodeKind<*>): Modifier.Node? {
125         visitNodes(type.mask, type.includeSelfInTraversal) {
126             return it
127         }
128         return null
129     }
130 
131     // Size exposed to LayoutCoordinates.
132     final override val size: IntSize
133         get() = measuredSize
134 
135     private var isClipping: Boolean = false
136 
137     protected var layerBlock: (GraphicsLayerScope.() -> Unit)? = null
138         private set
139 
140     private var layerDensity: Density = layoutNode.density
141     private var layerLayoutDirection: LayoutDirection = layoutNode.layoutDirection
142 
143     private var lastLayerAlpha: Float = 0.8f
144 
145     fun isTransparent(): Boolean {
146         if (layer != null && lastLayerAlpha <= 0f) return true
147         return this.wrappedBy?.isTransparent() ?: return false
148     }
149 
150     override val alignmentLinesOwner: AlignmentLinesOwner
151         get() = layoutNode.layoutDelegate.alignmentLinesOwner
152 
153     override val child: LookaheadCapablePlaceable?
154         get() = wrapped
155 
156     override fun replace() {
157         val explicitLayer = explicitLayer
158         if (explicitLayer != null) {
159             placeAt(position, zIndex, explicitLayer)
160         } else {
161             placeAt(position, zIndex, layerBlock)
162         }
163     }
164 
165     override val hasMeasureResult: Boolean
166         get() = _measureResult != null
167 
168     override val isAttached: Boolean
169         get() = tail.isAttached
170 
171     private var _measureResult: MeasureResult? = null
172     override var measureResult: MeasureResult
173         get() = _measureResult ?: error(UnmeasuredError)
174         internal set(value) {
175             val old = _measureResult
176             if (value !== old) {
177                 _measureResult = value
178                 if (old == null || value.width != old.width || value.height != old.height) {
179                     onMeasureResultChanged(value.width, value.height)
180                 }
181                 // We do not simply compare against old.alignmentLines in case this is a
182                 // MutableStateMap and the same instance might be passed.
183                 if (
184                     ((oldAlignmentLines != null && oldAlignmentLines!!.isNotEmpty()) ||
185                         value.alignmentLines.isNotEmpty()) &&
186                         !compareEquals(oldAlignmentLines, value.alignmentLines)
187                 ) {
188                     alignmentLinesOwner.alignmentLines.onAlignmentsChanged()
189 
190                     @Suppress("PrimitiveInCollection")
191                     val oldLines =
192                         oldAlignmentLines
193                             ?: (mutableObjectIntMapOf<AlignmentLine>().also {
194                                 oldAlignmentLines = it
195                             })
196                     oldLines.clear()
197                     value.alignmentLines.forEach { entry -> oldLines[entry.key] = entry.value }
198                 }
199             }
200         }
201 
202     abstract var lookaheadDelegate: LookaheadDelegate?
203         protected set
204 
205     private var oldAlignmentLines: MutableObjectIntMap<AlignmentLine>? = null
206 
207     abstract fun ensureLookaheadDelegateCreated()
208 
209     override val providedAlignmentLines: Set<AlignmentLine>
210         get() {
211             var set: MutableSet<AlignmentLine>? = null
212             var coordinator: NodeCoordinator? = this
213             while (coordinator != null) {
214                 val alignmentLines = coordinator._measureResult?.alignmentLines
215                 if (alignmentLines?.isNotEmpty() == true) {
216                     if (set == null) {
217                         set = mutableSetOf()
218                     }
219                     set.addAll(alignmentLines.keys)
220                 }
221                 coordinator = coordinator.wrapped
222             }
223             return set ?: emptySet()
224         }
225 
226     /**
227      * Called when the width or height of [measureResult] change. The object instance pointed to by
228      * [measureResult] may or may not have changed.
229      */
230     protected open fun onMeasureResultChanged(width: Int, height: Int) {
231         val layer = layer
232         if (layer != null) {
233             layer.resize(IntSize(width, height))
234         } else {
235             // if the node is not placed then this change will not be visible
236             if (layoutNode.isPlaced) {
237                 wrappedBy?.invalidateLayer()
238             }
239         }
240         measuredSize = IntSize(width, height)
241         if (layerBlock != null) {
242             updateLayerParameters(invokeOnLayoutChange = false)
243         }
244         visitNodes(Nodes.Draw) { it.onMeasureResultChanged() }
245         layoutNode.owner?.onLayoutChange(layoutNode)
246     }
247 
248     override var position: IntOffset = IntOffset.Zero
249         protected set
250 
251     var zIndex: Float = 0f
252         protected set
253 
254     override val parentData: Any?
255         get() {
256             // NOTE: If you make changes to this getter, please check the generated bytecode to
257             // ensure no extra allocation is made. See the note below.
258             if (layoutNode.nodes.has(Nodes.ParentData)) {
259                 val thisNode = tail
260                 // NOTE: Keep this mutable variable scoped inside the if statement. When moved
261                 // to the outer scope of get(), this causes the compiler to generate a
262                 // Ref$ObjectRef instance on every call of this getter.
263                 var data: Any? = null
264                 layoutNode.nodes.tailToHead { node ->
265                     if (node.isKind(Nodes.ParentData)) {
266                         node.dispatchForKind(Nodes.ParentData) {
267                             data = with(it) { layoutNode.density.modifyParentData(data) }
268                         }
269                     }
270                     if (node === thisNode) return@tailToHead
271                 }
272                 return data
273             }
274             return null
275         }
276 
277     internal fun onCoordinatesUsed() {
278         layoutNode.layoutDelegate.onCoordinatesUsed()
279     }
280 
281     final override val parentLayoutCoordinates: LayoutCoordinates?
282         get() {
283             checkPrecondition(isAttached) { ExpectAttachedLayoutCoordinates }
284             onCoordinatesUsed()
285             return layoutNode.outerCoordinator.wrappedBy
286         }
287 
288     final override val parentCoordinates: LayoutCoordinates?
289         get() {
290             checkPrecondition(isAttached) { ExpectAttachedLayoutCoordinates }
291             onCoordinatesUsed()
292             return wrappedBy
293         }
294 
295     private var _rectCache: MutableRect? = null
296     protected val rectCache: MutableRect
297         get() = _rectCache ?: MutableRect(0f, 0f, 0f, 0f).also { _rectCache = it }
298 
299     private val snapshotObserver
300         get() = layoutNode.requireOwner().snapshotObserver
301 
302     /** The current layer's positional attributes. */
303     private var layerPositionalProperties: LayerPositionalProperties? = null
304 
305     internal val lastMeasurementConstraints: Constraints
306         get() = measurementConstraints
307 
308     protected inline fun performingMeasure(
309         constraints: Constraints,
310         crossinline block: () -> Placeable
311     ): Placeable {
312         measurementConstraints = constraints
313         return block()
314     }
315 
316     fun onMeasured() {
317         if (hasNode(Nodes.LayoutAware)) {
318             Snapshot.withoutReadObservation {
319                 visitNodes(Nodes.LayoutAware) { it.onRemeasured(measuredSize) }
320             }
321         }
322     }
323 
324     fun onUnplaced() {
325         if (hasNode(Nodes.Unplaced)) {
326             visitNodes(Nodes.Unplaced) { it.onUnplaced() }
327         }
328     }
329 
330     /** Places the modified child. */
331     /*@CallSuper*/
332     override fun placeAt(
333         position: IntOffset,
334         zIndex: Float,
335         layerBlock: (GraphicsLayerScope.() -> Unit)?
336     ) {
337         if (forcePlaceWithLookaheadOffset) {
338             placeSelf(lookaheadDelegate!!.position, zIndex, layerBlock, null)
339         } else {
340             placeSelf(position, zIndex, layerBlock, null)
341         }
342     }
343 
344     override fun placeAt(position: IntOffset, zIndex: Float, layer: GraphicsLayer) {
345         if (forcePlaceWithLookaheadOffset) {
346             placeSelf(lookaheadDelegate!!.position, zIndex, null, layer)
347         } else {
348             placeSelf(position, zIndex, null, layer)
349         }
350     }
351 
352     private fun placeSelf(
353         position: IntOffset,
354         zIndex: Float,
355         layerBlock: (GraphicsLayerScope.() -> Unit)?,
356         explicitLayer: GraphicsLayer?
357     ) {
358         if (explicitLayer != null) {
359             requirePrecondition(layerBlock == null) {
360                 "both ways to create layers shouldn't be used together"
361             }
362             if (this.explicitLayer !== explicitLayer) {
363                 // reset previous layer object first if the explicitLayer changed
364                 this.explicitLayer = null
365                 updateLayerBlock(null)
366                 this.explicitLayer = explicitLayer
367             }
368             if (layer == null) {
369                 layer =
370                     layoutNode
371                         .requireOwner()
372                         .createLayer(drawBlock, invalidateParentLayer, explicitLayer)
373                         .apply {
374                             resize(measuredSize)
375                             move(position)
376                         }
377                 layoutNode.innerLayerCoordinatorIsDirty = true
378                 invalidateParentLayer()
379             }
380         } else {
381             if (this.explicitLayer != null) {
382                 this.explicitLayer = null
383                 // we need to first release the OwnedLayer created for explicitLayer
384                 // as we don't support updating the same OwnedLayer object from using
385                 // explicit layer to implicit one.
386                 updateLayerBlock(null)
387             }
388             updateLayerBlock(layerBlock)
389         }
390         if (this.position != position) {
391             layoutNode.requireOwner().voteFrameRate(FrameRateCategory.High.value)
392             this.position = position
393             layoutNode.layoutDelegate.measurePassDelegate
394                 .notifyChildrenUsingCoordinatesWhilePlacing()
395             val layer = layer
396             if (layer != null) {
397                 layer.move(position)
398             } else {
399                 wrappedBy?.invalidateLayer()
400             }
401             invalidateAlignmentLinesFromPositionChange()
402             layoutNode.owner?.onLayoutChange(layoutNode)
403         }
404         this.zIndex = zIndex
405         if (!isPlacingForAlignment) {
406             captureRulersIfNeeded(measureResult)
407         }
408     }
409 
410     fun releaseLayer() {
411         if (layer != null) {
412             if (explicitLayer != null) {
413                 explicitLayer = null
414             }
415             updateLayerBlock(null)
416 
417             // as we removed the layer the node was placed with, we have to request relayout in
418             // case the node will be reused in future. during the relayout the layer will be
419             // recreated again if needed.
420             layoutNode.requestRelayout()
421         }
422     }
423 
424     fun placeSelfApparentToRealOffset(
425         position: IntOffset,
426         zIndex: Float,
427         layerBlock: (GraphicsLayerScope.() -> Unit)?,
428         layer: GraphicsLayer?
429     ) {
430         placeSelf(position + apparentToRealOffset, zIndex, layerBlock, layer)
431     }
432 
433     /** Draws the content of the LayoutNode */
434     fun draw(canvas: Canvas, graphicsLayer: GraphicsLayer?) {
435         val layer = layer
436         if (layer != null) {
437             layer.drawLayer(canvas, graphicsLayer)
438         } else {
439             val x = position.x.toFloat()
440             val y = position.y.toFloat()
441             canvas.translate(x, y)
442             drawContainedDrawModifiers(canvas, graphicsLayer)
443             canvas.translate(-x, -y)
444         }
445     }
446 
447     private fun drawContainedDrawModifiers(canvas: Canvas, graphicsLayer: GraphicsLayer?) {
448         val head = head(Nodes.Draw)
449         if (head == null) {
450             performDraw(canvas, graphicsLayer)
451         } else {
452             val drawScope = layoutNode.mDrawScope
453             drawScope.draw(canvas, size.toSize(), this, head, graphicsLayer)
454         }
455     }
456 
457     open fun performDraw(canvas: Canvas, graphicsLayer: GraphicsLayer?) {
458         wrapped?.draw(canvas, graphicsLayer)
459     }
460 
461     fun onPlaced() {
462         visitNodes(Nodes.LayoutAware) { it.onPlaced(this) }
463     }
464 
465     private var drawBlockParentLayer: GraphicsLayer? = null
466     private var drawBlockCanvas: Canvas? = null
467 
468     private var _drawBlock: ((Canvas, GraphicsLayer?) -> Unit)? = null
469 
470     // implementation of draw block passed to the OwnedLayer
471     private val drawBlock: (Canvas, GraphicsLayer?) -> Unit
472         get() {
473             var block = _drawBlock
474             if (block == null) {
475                 val drawBlockCallToDrawModifiers = {
476                     drawContainedDrawModifiers(drawBlockCanvas!!, drawBlockParentLayer)
477                 }
478                 block = { canvas, parentLayer ->
479                     if (layoutNode.isPlaced) {
480                         this.drawBlockCanvas = canvas
481                         this.drawBlockParentLayer = parentLayer
482                         snapshotObserver.observeReads(
483                             this,
484                             onCommitAffectingLayer,
485                             drawBlockCallToDrawModifiers
486                         )
487                         lastLayerDrawingWasSkipped = false
488                     } else {
489                         // The invalidation is requested even for nodes which are not placed. As we
490                         // are not going to display them we skip the drawing. It is safe to just
491                         // draw nothing as the layer will be invalidated again when the node will be
492                         // finally placed.
493                         lastLayerDrawingWasSkipped = true
494                     }
495                 }
496                 _drawBlock = block
497             }
498             return block
499         }
500 
501     fun updateLayerBlock(
502         layerBlock: (GraphicsLayerScope.() -> Unit)?,
503         forceUpdateLayerParameters: Boolean = false
504     ) {
505         requirePrecondition(layerBlock == null || explicitLayer == null) {
506             "layerBlock can't be provided when explicitLayer is provided"
507         }
508         val layoutNode = layoutNode
509         val updateParameters =
510             forceUpdateLayerParameters ||
511                 this.layerBlock !== layerBlock ||
512                 layerDensity != layoutNode.density ||
513                 layerLayoutDirection != layoutNode.layoutDirection
514         this.layerDensity = layoutNode.density
515         this.layerLayoutDirection = layoutNode.layoutDirection
516 
517         if (layoutNode.isAttached && layerBlock != null) {
518             this.layerBlock = layerBlock
519             if (layer == null) {
520                 layer =
521                     layoutNode
522                         .requireOwner()
523                         .createLayer(
524                             drawBlock,
525                             invalidateParentLayer,
526                             forceUseOldLayers = layoutNode.forceUseOldLayers
527                         )
528                         .apply {
529                             resize(measuredSize)
530                             move(position)
531                         }
532                 updateLayerParameters()
533                 layoutNode.innerLayerCoordinatorIsDirty = true
534                 invalidateParentLayer()
535             } else if (updateParameters) {
536                 val positionalPropertiesChanged = updateLayerParameters()
537                 if (positionalPropertiesChanged) {
538                     layoutNode
539                         .requireOwner()
540                         .rectManager
541                         .onLayoutLayerPositionalPropertiesChanged(layoutNode)
542                 }
543             }
544         } else {
545             this.layerBlock = null
546             layer?.let {
547                 it.destroy()
548                 layoutNode.innerLayerCoordinatorIsDirty = true
549                 invalidateParentLayer()
550                 if (isAttached && layoutNode.isPlaced) {
551                     layoutNode.owner?.onLayoutChange(layoutNode)
552                 }
553             }
554             layer = null
555             lastLayerDrawingWasSkipped = false
556         }
557     }
558 
559     /** returns true if some of the positional properties did change. */
560     private fun updateLayerParameters(invokeOnLayoutChange: Boolean = true): Boolean {
561         if (explicitLayer != null) {
562             // the parameters of the explicit layers are configured differently.
563             return false
564         }
565         val layer = layer
566         if (layer != null) {
567             val layerBlock =
568                 checkPreconditionNotNull(layerBlock) {
569                     "updateLayerParameters requires a non-null layerBlock"
570                 }
571             graphicsLayerScope.reset()
572             graphicsLayerScope.graphicsDensity = layoutNode.density
573             graphicsLayerScope.layoutDirection = layoutNode.layoutDirection
574             graphicsLayerScope.size = size.toSize()
575             snapshotObserver.observeReads(this, onCommitAffectingLayerParams) {
576                 layerBlock.invoke(graphicsLayerScope)
577                 graphicsLayerScope.updateOutline()
578             }
579             val layerPositionalProperties =
580                 layerPositionalProperties
581                     ?: LayerPositionalProperties().also { layerPositionalProperties = it }
582             tmpLayerPositionalProperties.copyFrom(layerPositionalProperties)
583             layerPositionalProperties.copyFrom(graphicsLayerScope)
584             layer.updateLayerProperties(graphicsLayerScope)
585             val wasClipping = isClipping
586             isClipping = graphicsLayerScope.clip
587             lastLayerAlpha = graphicsLayerScope.alpha
588             val positionalPropertiesChanged =
589                 !tmpLayerPositionalProperties.hasSameValuesAs(layerPositionalProperties)
590             if (
591                 invokeOnLayoutChange && (positionalPropertiesChanged || wasClipping != isClipping)
592             ) {
593                 layoutNode.owner?.onLayoutChange(layoutNode)
594             }
595             return positionalPropertiesChanged
596         } else {
597             checkPrecondition(layerBlock == null) { "null layer with a non-null layerBlock" }
598             return false
599         }
600     }
601 
602     private val invalidateParentLayer: () -> Unit = { wrappedBy?.invalidateLayer() }
603 
604     /**
605      * True when the last drawing of this layer didn't draw the real content as the LayoutNode
606      * containing this layer was not placed by the parent.
607      */
608     internal var lastLayerDrawingWasSkipped = false
609         private set
610 
611     var layer: OwnedLayer? = null
612         private set
613 
614     private var explicitLayer: GraphicsLayer? = null
615 
616     override val isValidOwnerScope: Boolean
617         get() = layer != null && !released && layoutNode.isAttached
618 
619     val minimumTouchTargetSize: Size
620         get() = with(layerDensity) { layoutNode.viewConfiguration.minimumTouchTargetSize.toSize() }
621 
622     fun onAttach() {
623         if (layer == null && layerBlock != null) {
624             // This has been detached and is now being reattached. It previously had a layer, so
625             // reconstitute one.
626             layer =
627                 layoutNode
628                     .requireOwner()
629                     .createLayer(drawBlock, invalidateParentLayer, explicitLayer)
630                     .apply {
631                         resize(measuredSize)
632                         move(position)
633                         invalidate()
634                     }
635         }
636     }
637 
638     fun onDetach() {
639         layer?.destroy()
640         layer = null
641     }
642 
643     /**
644      * Executes a hit test for this [NodeCoordinator].
645      *
646      * @param hitTestSource The hit test specifics for pointer input or semantics
647      * @param pointerPosition The tested pointer position, which is relative to the
648      *   [NodeCoordinator].
649      * @param hitTestResult The parent [HitTestResult] that any hit should be added to.
650      * @param pointerType The [PointerType] of the source input. Touch sources allow for minimum
651      *   touch target. Semantics hit tests always treat hits as needing minimum touch target.
652      * @param isInLayer `true` if the touch event is in the layer of this and all parents or `false`
653      *   if it is outside the layer, but within the minimum touch target of the edge of the layer.
654      *   This can only be `false` when [pointerType] is [PointerType.Touch] or else a layer miss
655      *   means the event will be clipped out.
656      */
657     fun hitTest(
658         hitTestSource: HitTestSource,
659         pointerPosition: Offset,
660         hitTestResult: HitTestResult,
661         pointerType: PointerType,
662         isInLayer: Boolean
663     ) {
664         val head = head(hitTestSource.entityType())
665         if (!withinLayerBounds(pointerPosition)) {
666             // This missed the clip, but if this layout is too small and this is within the
667             // minimum touch target, we still consider it a hit.
668             if (pointerType == PointerType.Touch) {
669                 val distanceFromEdge =
670                     distanceInMinimumTouchTarget(pointerPosition, minimumTouchTargetSize)
671                 if (
672                     distanceFromEdge.fastIsFinite() &&
673                         hitTestResult.isHitInMinimumTouchTargetBetter(distanceFromEdge, false)
674                 ) {
675                     head.hitNear(
676                         hitTestSource,
677                         pointerPosition,
678                         hitTestResult,
679                         pointerType,
680                         false,
681                         distanceFromEdge
682                     )
683                 } // else it is a complete miss.
684             }
685         } else if (head == null) {
686             hitTestChild(hitTestSource, pointerPosition, hitTestResult, pointerType, isInLayer)
687         } else if (isPointerInBounds(pointerPosition)) {
688             // A real hit
689             head.hit(hitTestSource, pointerPosition, hitTestResult, pointerType, isInLayer)
690         } else {
691             val distanceFromEdge =
692                 if (pointerType != PointerType.Touch) Float.POSITIVE_INFINITY
693                 else {
694                     distanceInMinimumTouchTarget(pointerPosition, minimumTouchTargetSize)
695                 }
696             val isHitInMinimumTouchTargetBetter =
697                 distanceFromEdge.fastIsFinite() &&
698                     hitTestResult.isHitInMinimumTouchTargetBetter(distanceFromEdge, isInLayer)
699 
700             head.outOfBoundsHit(
701                 hitTestSource,
702                 pointerPosition,
703                 hitTestResult,
704                 pointerType,
705                 isInLayer,
706                 distanceFromEdge,
707                 isHitInMinimumTouchTargetBetter
708             )
709         }
710     }
711 
712     /**
713      * The [NodeCoordinator] had a hit in bounds and can record any children in the [hitTestResult].
714      */
715     private fun Modifier.Node?.hit(
716         hitTestSource: HitTestSource,
717         pointerPosition: Offset,
718         hitTestResult: HitTestResult,
719         pointerType: PointerType,
720         isInLayer: Boolean
721     ) {
722         if (this == null) {
723             hitTestChild(hitTestSource, pointerPosition, hitTestResult, pointerType, isInLayer)
724         } else {
725             hitTestResult.hit(this, isInLayer) {
726                 nextUntil(hitTestSource.entityType(), Nodes.Layout)
727                     .hit(hitTestSource, pointerPosition, hitTestResult, pointerType, isInLayer)
728             }
729         }
730     }
731 
732     /**
733      * The pointer lands outside the node's bounds. There are three cases we have to handle:
734      * 1. hitNear: if the nodes is smaller than the minimumTouchTargetSize, it's touch bounds will
735      *    be expanded to the minimal touch target size.
736      * 2. hitExpandedTouchBounds: if the nodes has a expanded touch bounds.
737      * 3. speculativeHit: if the hit misses this node, but its child can still get the pointer
738      *    event.
739      *
740      * The complication is when touch bounds overlaps, there are 3 possibilities:
741      * 1. hit in this node's expanded touch bounds or minimum touch target bounds overlaps with a
742      *    direct hit in the other node. The node with direct hit will get the event.
743      * 2. hit in this node's expanded touch bounds overlaps with other node's expanded touch bounds.
744      *    Both nodes will get the event.
745      * 3. hit in this node's expanded touch bounds overlaps with the other node's minimum touch
746      *    touch bounds. The node with expanded touch bounds will get the event.
747      *
748      * The logic to handle the hit priority is implemented in [HitTestResult.speculativeHit] and
749      * [HitTestResult.hitExpandedTouchBounds].
750      */
751     private fun Modifier.Node?.outOfBoundsHit(
752         hitTestSource: HitTestSource,
753         pointerPosition: Offset,
754         hitTestResult: HitTestResult,
755         pointerType: PointerType,
756         isInLayer: Boolean,
757         distanceFromEdge: Float,
758         isHitInMinimumTouchTargetBetter: Boolean
759     ) {
760         if (this == null) {
761             hitTestChild(hitTestSource, pointerPosition, hitTestResult, pointerType, isInLayer)
762         } else if (isInExpandedTouchBounds(pointerPosition, pointerType)) {
763             hitTestResult.hitExpandedTouchBounds(this, isInLayer) {
764                 nextUntil(hitTestSource.entityType(), Nodes.Layout)
765                     .outOfBoundsHit(
766                         hitTestSource,
767                         pointerPosition,
768                         hitTestResult,
769                         pointerType,
770                         isInLayer,
771                         distanceFromEdge,
772                         isHitInMinimumTouchTargetBetter
773                     )
774             }
775         } else if (isHitInMinimumTouchTargetBetter) {
776             hitNear(
777                 hitTestSource,
778                 pointerPosition,
779                 hitTestResult,
780                 pointerType,
781                 isInLayer,
782                 distanceFromEdge
783             )
784         } else {
785             speculativeHit(
786                 hitTestSource,
787                 pointerPosition,
788                 hitTestResult,
789                 pointerType,
790                 isInLayer,
791                 distanceFromEdge
792             )
793         }
794     }
795 
796     /**
797      * The [NodeCoordinator] had a hit [distanceFromEdge] from the bounds and it is within the
798      * minimum touch target distance, so it should be recorded as such in the [hitTestResult].
799      */
800     private fun Modifier.Node?.hitNear(
801         hitTestSource: HitTestSource,
802         pointerPosition: Offset,
803         hitTestResult: HitTestResult,
804         pointerType: PointerType,
805         isInLayer: Boolean,
806         distanceFromEdge: Float
807     ) {
808         if (this == null) {
809             hitTestChild(hitTestSource, pointerPosition, hitTestResult, pointerType, isInLayer)
810         } else {
811             // Hit closer than existing handlers, so just record it
812             hitTestResult.hitInMinimumTouchTarget(this, distanceFromEdge, isInLayer) {
813                 nextUntil(hitTestSource.entityType(), Nodes.Layout)
814                     .outOfBoundsHit(
815                         hitTestSource,
816                         pointerPosition,
817                         hitTestResult,
818                         pointerType,
819                         isInLayer,
820                         distanceFromEdge,
821                         isHitInMinimumTouchTargetBetter = true
822                     )
823             }
824         }
825     }
826 
827     /**
828      * The [NodeCoordinator] had a miss, but it hasn't been clipped out. The child must be checked
829      * to see if it hit.
830      */
831     private fun Modifier.Node?.speculativeHit(
832         hitTestSource: HitTestSource,
833         pointerPosition: Offset,
834         hitTestResult: HitTestResult,
835         pointerType: PointerType,
836         isInLayer: Boolean,
837         distanceFromEdge: Float
838     ) {
839         if (this == null) {
840             hitTestChild(hitTestSource, pointerPosition, hitTestResult, pointerType, isInLayer)
841         } else if (hitTestSource.interceptOutOfBoundsChildEvents(this)) {
842             // We only want to replace the existing touch target if there are better
843             // hits in the children
844             hitTestResult.speculativeHit(this, distanceFromEdge, isInLayer) {
845                 nextUntil(hitTestSource.entityType(), Nodes.Layout)
846                     .outOfBoundsHit(
847                         hitTestSource,
848                         pointerPosition,
849                         hitTestResult,
850                         pointerType,
851                         isInLayer,
852                         distanceFromEdge,
853                         isHitInMinimumTouchTargetBetter = false
854                     )
855             }
856         } else {
857             nextUntil(hitTestSource.entityType(), Nodes.Layout)
858                 .outOfBoundsHit(
859                     hitTestSource,
860                     pointerPosition,
861                     hitTestResult,
862                     pointerType,
863                     isInLayer,
864                     distanceFromEdge,
865                     isHitInMinimumTouchTargetBetter = false
866                 )
867         }
868     }
869 
870     /**
871      * Helper method to check if the pointer is inside the node's expanded touch bounds. This only
872      * applies to pointer input modifier nodes whose [PointerInputModifierNode.touchBoundsExpansion]
873      * is not null.
874      */
875     private fun Modifier.Node?.isInExpandedTouchBounds(
876         pointerPosition: Offset,
877         pointerType: PointerType
878     ): Boolean {
879         if (this == null) {
880             return false
881         }
882         // The expanded touch bounds only works for stylus at this moment.
883         if (pointerType != PointerType.Stylus && pointerType != PointerType.Eraser) {
884             return false
885         }
886         dispatchForKind(Nodes.PointerInput) {
887             // We only check for the node itself or the first delegate PointerInputModifierNode.
888             val expansion = it.touchBoundsExpansion
889             return pointerPosition.x >= -expansion.computeLeft(layoutDirection) &&
890                 pointerPosition.x < measuredWidth + expansion.computeRight(layoutDirection) &&
891                 pointerPosition.y >= -expansion.top &&
892                 pointerPosition.y < measuredHeight + expansion.bottom
893         }
894         return false
895     }
896 
897     /** Do a [hitTest] on the children of this [NodeCoordinator]. */
898     open fun hitTestChild(
899         hitTestSource: HitTestSource,
900         pointerPosition: Offset,
901         hitTestResult: HitTestResult,
902         pointerType: PointerType,
903         isInLayer: Boolean
904     ) {
905         // Also, keep looking to see if we also might hit any children.
906         // This avoids checking layer bounds twice as when we call super.hitTest()
907         val wrapped = wrapped
908         if (wrapped != null) {
909             val positionInWrapped = wrapped.fromParentPosition(pointerPosition)
910             wrapped.hitTest(hitTestSource, positionInWrapped, hitTestResult, pointerType, isInLayer)
911         }
912     }
913 
914     /** Returns the bounds of this [NodeCoordinator], including the minimum touch target. */
915     fun touchBoundsInRoot(): Rect {
916         if (!isAttached) {
917             return Rect.Zero
918         }
919 
920         val root = findRootCoordinates()
921 
922         val bounds = rectCache
923         val padding = calculateMinimumTouchTargetPadding(minimumTouchTargetSize)
924         bounds.left = -padding.width
925         bounds.top = -padding.height
926         bounds.right = measuredWidth + padding.width
927         bounds.bottom = measuredHeight + padding.height
928 
929         var coordinator: NodeCoordinator = this
930         while (coordinator !== root) {
931             coordinator.rectInParent(
932                 bounds,
933                 clipBounds = false,
934                 clipToMinimumTouchTargetSize = true
935             )
936             if (bounds.isEmpty) {
937                 return Rect.Zero
938             }
939 
940             coordinator = coordinator.wrappedBy!!
941         }
942         return bounds.toRect()
943     }
944 
945     override fun screenToLocal(relativeToScreen: Offset): Offset {
946         checkPrecondition(isAttached) { ExpectAttachedLayoutCoordinates }
947         val owner = layoutNode.requireOwner()
948         val positionInRoot = owner.screenToLocal(relativeToScreen)
949         val root = findRootCoordinates()
950         return localPositionOf(root, positionInRoot)
951     }
952 
953     override fun localToScreen(relativeToLocal: Offset): Offset {
954         checkPrecondition(isAttached) { ExpectAttachedLayoutCoordinates }
955         val positionInRoot = localToRoot(relativeToLocal)
956         val owner = layoutNode.requireOwner()
957         return owner.localToScreen(positionInRoot)
958     }
959 
960     override fun windowToLocal(relativeToWindow: Offset): Offset {
961         checkPrecondition(isAttached) { ExpectAttachedLayoutCoordinates }
962         val root = findRootCoordinates()
963         val positionInRoot =
964             layoutNode.requireOwner().calculateLocalPosition(relativeToWindow) -
965                 root.positionInRoot()
966         return localPositionOf(root, positionInRoot)
967     }
968 
969     override fun localToWindow(relativeToLocal: Offset): Offset {
970         val positionInRoot = localToRoot(relativeToLocal)
971         val owner = layoutNode.requireOwner()
972         return owner.calculatePositionInWindow(positionInRoot)
973     }
974 
975     private fun LayoutCoordinates.toCoordinator() =
976         (this as? LookaheadLayoutCoordinates)?.coordinator ?: this as NodeCoordinator
977 
978     override fun localPositionOf(
979         sourceCoordinates: LayoutCoordinates,
980         relativeToSource: Offset
981     ): Offset =
982         localPositionOf(
983             sourceCoordinates = sourceCoordinates,
984             relativeToSource = relativeToSource,
985             includeMotionFrameOfReference = true
986         )
987 
988     override fun localPositionOf(
989         sourceCoordinates: LayoutCoordinates,
990         relativeToSource: Offset,
991         includeMotionFrameOfReference: Boolean
992     ): Offset {
993         if (sourceCoordinates is LookaheadLayoutCoordinates) {
994             sourceCoordinates.coordinator.onCoordinatesUsed()
995             return -sourceCoordinates.localPositionOf(
996                 sourceCoordinates = this,
997                 relativeToSource = -relativeToSource,
998                 includeMotionFrameOfReference = includeMotionFrameOfReference
999             )
1000         }
1001 
1002         val nodeCoordinator = sourceCoordinates.toCoordinator()
1003         nodeCoordinator.onCoordinatesUsed()
1004         val commonAncestor = findCommonAncestor(nodeCoordinator)
1005 
1006         var position = relativeToSource
1007         var coordinator = nodeCoordinator
1008         while (coordinator !== commonAncestor) {
1009             position = coordinator.toParentPosition(position, includeMotionFrameOfReference)
1010             coordinator = coordinator.wrappedBy!!
1011         }
1012 
1013         return ancestorToLocal(commonAncestor, position, includeMotionFrameOfReference)
1014     }
1015 
1016     override fun transformFrom(sourceCoordinates: LayoutCoordinates, matrix: Matrix) {
1017         val coordinator = sourceCoordinates.toCoordinator()
1018         coordinator.onCoordinatesUsed()
1019         val commonAncestor = findCommonAncestor(coordinator)
1020 
1021         matrix.reset()
1022         // Transform from the source to the common ancestor
1023         coordinator.transformToAncestor(commonAncestor, matrix)
1024         // Transform from the common ancestor to this
1025         transformFromAncestor(commonAncestor, matrix)
1026     }
1027 
1028     override fun transformToScreen(matrix: Matrix) {
1029         val owner = layoutNode.requireOwner()
1030         val rootCoordinator = findRootCoordinates().toCoordinator()
1031         transformToAncestor(rootCoordinator, matrix)
1032         if (owner is MatrixPositionCalculator) {
1033             // Only Android owner supports direct matrix manipulations,
1034             // This API had to be Android-only in the first place.
1035             owner.localToScreen(matrix)
1036         } else {
1037             // Fallback: try to extract just position
1038             val screenPosition = rootCoordinator.positionOnScreen()
1039             if (screenPosition.isSpecified) {
1040                 matrix.translate(screenPosition.x, screenPosition.y, 0f)
1041             }
1042         }
1043     }
1044 
1045     private fun transformToAncestor(ancestor: NodeCoordinator, matrix: Matrix) {
1046         var wrapper = this
1047         while (wrapper != ancestor) {
1048             wrapper.layer?.transform(matrix)
1049             val position = wrapper.position
1050             if (position != IntOffset.Zero) {
1051                 tmpMatrix.reset()
1052                 tmpMatrix.translate(position.x.toFloat(), position.y.toFloat())
1053                 matrix.timesAssign(tmpMatrix)
1054             }
1055             wrapper = wrapper.wrappedBy!!
1056         }
1057     }
1058 
1059     private fun transformFromAncestor(ancestor: NodeCoordinator, matrix: Matrix) {
1060         if (ancestor != this) {
1061             wrappedBy!!.transformFromAncestor(ancestor, matrix)
1062             if (position != IntOffset.Zero) {
1063                 tmpMatrix.reset()
1064                 tmpMatrix.translate(-position.x.toFloat(), -position.y.toFloat())
1065                 matrix.timesAssign(tmpMatrix)
1066             }
1067             layer?.inverseTransform(matrix)
1068         }
1069     }
1070 
1071     override fun localBoundingBoxOf(
1072         sourceCoordinates: LayoutCoordinates,
1073         clipBounds: Boolean
1074     ): Rect {
1075         checkPrecondition(isAttached) { ExpectAttachedLayoutCoordinates }
1076         checkPrecondition(sourceCoordinates.isAttached) {
1077             "LayoutCoordinates $sourceCoordinates is not attached!"
1078         }
1079         val srcCoordinator = sourceCoordinates.toCoordinator()
1080         srcCoordinator.onCoordinatesUsed()
1081         val commonAncestor = findCommonAncestor(srcCoordinator)
1082 
1083         val bounds = rectCache
1084         bounds.left = 0f
1085         bounds.top = 0f
1086         bounds.right = sourceCoordinates.size.width.toFloat()
1087         bounds.bottom = sourceCoordinates.size.height.toFloat()
1088 
1089         var coordinator = srcCoordinator
1090         while (coordinator !== commonAncestor) {
1091             coordinator.rectInParent(bounds, clipBounds)
1092             if (bounds.isEmpty) {
1093                 return Rect.Zero
1094             }
1095 
1096             coordinator = coordinator.wrappedBy!!
1097         }
1098 
1099         ancestorToLocal(commonAncestor, bounds, clipBounds)
1100         return bounds.toRect()
1101     }
1102 
1103     private fun ancestorToLocal(
1104         ancestor: NodeCoordinator,
1105         offset: Offset,
1106         includeMotionFrameOfReference: Boolean,
1107     ): Offset {
1108         if (ancestor === this) {
1109             return offset
1110         }
1111         val wrappedBy = wrappedBy
1112         if (wrappedBy == null || ancestor == wrappedBy) {
1113             return fromParentPosition(offset, includeMotionFrameOfReference)
1114         }
1115         return fromParentPosition(
1116             position = wrappedBy.ancestorToLocal(ancestor, offset, includeMotionFrameOfReference),
1117             includeMotionFrameOfReference = includeMotionFrameOfReference
1118         )
1119     }
1120 
1121     private fun ancestorToLocal(ancestor: NodeCoordinator, rect: MutableRect, clipBounds: Boolean) {
1122         if (ancestor === this) {
1123             return
1124         }
1125         wrappedBy?.ancestorToLocal(ancestor, rect, clipBounds)
1126         return fromParentRect(rect, clipBounds)
1127     }
1128 
1129     override fun localToRoot(relativeToLocal: Offset): Offset {
1130         checkPrecondition(isAttached) { ExpectAttachedLayoutCoordinates }
1131         onCoordinatesUsed()
1132         var coordinator: NodeCoordinator? = this
1133         var position = relativeToLocal
1134         while (coordinator != null) {
1135             position = coordinator.toParentPosition(position)
1136             coordinator = coordinator.wrappedBy
1137         }
1138         return position
1139     }
1140 
1141     protected inline fun withPositionTranslation(canvas: Canvas, block: (Canvas) -> Unit) {
1142         val x = position.x.toFloat()
1143         val y = position.y.toFloat()
1144         canvas.translate(x, y)
1145         block(canvas)
1146         canvas.translate(-x, -y)
1147     }
1148 
1149     /**
1150      * Converts [position] in the local coordinate system to a [Offset] in the
1151      * [parentLayoutCoordinates] coordinate system.
1152      */
1153     open fun toParentPosition(
1154         position: Offset,
1155         includeMotionFrameOfReference: Boolean = true
1156     ): Offset {
1157         val layer = layer
1158         val targetPosition = layer?.mapOffset(position, inverse = false) ?: position
1159         return if (!includeMotionFrameOfReference && isPlacedUnderMotionFrameOfReference) {
1160             targetPosition
1161         } else {
1162             targetPosition + this.position
1163         }
1164     }
1165 
1166     /**
1167      * Converts [position] in the [parentLayoutCoordinates] coordinate system to a [Offset] in the
1168      * local coordinate system.
1169      */
1170     open fun fromParentPosition(
1171         position: Offset,
1172         includeMotionFrameOfReference: Boolean = true
1173     ): Offset {
1174         val relativeToPosition =
1175             if (!includeMotionFrameOfReference && this.isPlacedUnderMotionFrameOfReference) {
1176                 position
1177             } else {
1178                 position - this.position
1179             }
1180         val layer = layer
1181         return layer?.mapOffset(relativeToPosition, inverse = true) ?: relativeToPosition
1182     }
1183 
1184     protected fun drawBorder(canvas: Canvas, paint: Paint) {
1185         canvas.drawRect(
1186             left = 0.5f,
1187             top = 0.5f,
1188             right = measuredSize.width.toFloat() - 0.5f,
1189             bottom = measuredSize.height.toFloat() - 0.5f,
1190             paint = paint
1191         )
1192     }
1193 
1194     /**
1195      * This will be called when the [LayoutNode] associated with this [NodeCoordinator] is attached
1196      * to the [Owner].
1197      */
1198     fun onLayoutNodeAttach() {
1199         // this call will update the parameters of the layer (alpha, scale, etc)
1200         updateLayerBlock(layerBlock, forceUpdateLayerParameters = true)
1201         // this call will invalidate the content of the layer
1202         layer?.invalidate()
1203     }
1204 
1205     /**
1206      * This will be called when the [LayoutNode] associated with this [NodeCoordinator] is released
1207      * or when the [NodeCoordinator] is released (will not be used anymore).
1208      */
1209     fun onRelease() {
1210         released = true
1211         // It is important to call invalidateParentLayer() here, even though updateLayerBlock() may
1212         // call it. The reason is because we end up calling this from the bottom up, which means
1213         // that if we have two layout modifiers getting removed, where the parent one has a layer
1214         // and the bottom one doesn't, the parent layer gets invalidated but then removed, leaving
1215         // no layers invalidated. By always calling this, we ensure that after all nodes are
1216         // removed at least one layer is invalidated.
1217         invalidateParentLayer()
1218         releaseLayer()
1219     }
1220 
1221     /**
1222      * Modifies bounds to be in the parent NodeCoordinator's coordinates, including clipping, if
1223      * [clipBounds] is true. If [clipToMinimumTouchTargetSize] is true and the layer clips, then the
1224      * clip bounds are extended to allow minimum touch target extended area.
1225      */
1226     internal fun rectInParent(
1227         bounds: MutableRect,
1228         clipBounds: Boolean,
1229         clipToMinimumTouchTargetSize: Boolean = false
1230     ) {
1231         val layer = layer
1232         if (layer != null) {
1233             if (isClipping) {
1234                 if (clipToMinimumTouchTargetSize) {
1235                     val minTouch = minimumTouchTargetSize
1236                     val horz = minTouch.width / 2f
1237                     val vert = minTouch.height / 2f
1238                     bounds.intersect(
1239                         -horz,
1240                         -vert,
1241                         size.width.toFloat() + horz,
1242                         size.height.toFloat() + vert
1243                     )
1244                 } else if (clipBounds) {
1245                     bounds.intersect(0f, 0f, size.width.toFloat(), size.height.toFloat())
1246                 }
1247                 if (bounds.isEmpty) {
1248                     return
1249                 }
1250             }
1251             layer.mapBounds(bounds, inverse = false)
1252         }
1253 
1254         val x = position.x
1255         bounds.left += x
1256         bounds.right += x
1257 
1258         val y = position.y
1259         bounds.top += y
1260         bounds.bottom += y
1261     }
1262 
1263     /**
1264      * Modifies bounds in the parent's coordinates to be in this NodeCoordinator's coordinates,
1265      * including clipping, if [clipBounds] is true.
1266      */
1267     private fun fromParentRect(bounds: MutableRect, clipBounds: Boolean) {
1268         val x = position.x
1269         bounds.left -= x
1270         bounds.right -= x
1271 
1272         val y = position.y
1273         bounds.top -= y
1274         bounds.bottom -= y
1275 
1276         val layer = layer
1277         if (layer != null) {
1278             layer.mapBounds(bounds, inverse = true)
1279             if (isClipping && clipBounds) {
1280                 bounds.intersect(0f, 0f, size.width.toFloat(), size.height.toFloat())
1281                 if (bounds.isEmpty) {
1282                     return
1283                 }
1284             }
1285         }
1286     }
1287 
1288     protected fun withinLayerBounds(pointerPosition: Offset): Boolean {
1289         if (!pointerPosition.isFinite) {
1290             return false
1291         }
1292         val layer = layer
1293         return layer == null || !isClipping || layer.isInLayer(pointerPosition)
1294     }
1295 
1296     /**
1297      * Whether a pointer that is relative to the [NodeCoordinator] is in the bounds of this
1298      * NodeCoordinator.
1299      */
1300     protected fun isPointerInBounds(pointerPosition: Offset): Boolean {
1301         val x = pointerPosition.x
1302         val y = pointerPosition.y
1303         return x >= 0f && y >= 0f && x < measuredWidth && y < measuredHeight
1304     }
1305 
1306     /** Invalidates the layer that this coordinator will draw into. */
1307     open fun invalidateLayer() {
1308         val layer = layer
1309         if (layer != null) {
1310             layer.invalidate()
1311         } else {
1312             wrappedBy?.invalidateLayer()
1313         }
1314     }
1315 
1316     /**
1317      * Called when [LayoutNode.modifier] has changed and all the NodeCoordinators have been
1318      * configured.
1319      */
1320     open fun onLayoutModifierNodeChanged() {
1321         layer?.invalidate()
1322     }
1323 
1324     internal fun findCommonAncestor(other: NodeCoordinator): NodeCoordinator {
1325         var ancestor1 = other.layoutNode
1326         var ancestor2 = layoutNode
1327         if (ancestor1 === ancestor2) {
1328             val otherNode = other.tail
1329             // They are on the same node, but we don't know which is the deeper of the two
1330             tail.visitLocalAncestors(Nodes.Layout.mask) { if (it === otherNode) return other }
1331             return this
1332         }
1333 
1334         while (ancestor1.depth > ancestor2.depth) {
1335             ancestor1 = ancestor1.parent!!
1336         }
1337 
1338         while (ancestor2.depth > ancestor1.depth) {
1339             ancestor2 = ancestor2.parent!!
1340         }
1341 
1342         while (ancestor1 !== ancestor2) {
1343             val parent1 = ancestor1.parent
1344             val parent2 = ancestor2.parent
1345             if (parent1 == null || parent2 == null) {
1346                 throw IllegalArgumentException("layouts are not part of the same hierarchy")
1347             }
1348             ancestor1 = parent1
1349             ancestor2 = parent2
1350         }
1351 
1352         return when {
1353             ancestor2 === layoutNode -> this
1354             ancestor1 === other.layoutNode -> other
1355             else -> ancestor1.innerCoordinator
1356         }
1357     }
1358 
1359     fun shouldSharePointerInputWithSiblings(): Boolean {
1360         val start = headNode(Nodes.PointerInput.includeSelfInTraversal) ?: return false
1361 
1362         if (start.isAttached) {
1363             // We have to check both the self and local descendants, because the `start` can also
1364             // be a `PointerInputModifierNode` (when the first modifier node on the LayoutNode is
1365             // a `PointerInputModifierNode`).
1366             start.visitSelfAndLocalDescendants(Nodes.PointerInput) {
1367                 if (it.sharePointerInputWithSiblings()) return true
1368             }
1369         }
1370 
1371         return false
1372     }
1373 
1374     private fun offsetFromEdge(pointerPosition: Offset): Offset {
1375         val x = pointerPosition.x
1376         val horizontal = maxOf(0f, if (x < 0) -x else x - measuredWidth)
1377         val y = pointerPosition.y
1378         val vertical = maxOf(0f, if (y < 0) -y else y - measuredHeight)
1379 
1380         return Offset(horizontal, vertical)
1381     }
1382 
1383     /**
1384      * Returns the additional amount on the horizontal and vertical dimensions that this extends
1385      * beyond [width] and [height] on all sides. This takes into account [minimumTouchTargetSize]
1386      * and [measuredSize] vs. [width] and [height].
1387      */
1388     protected fun calculateMinimumTouchTargetPadding(minimumTouchTargetSize: Size): Size {
1389         val widthDiff = minimumTouchTargetSize.width - measuredWidth.toFloat()
1390         val heightDiff = minimumTouchTargetSize.height - measuredHeight.toFloat()
1391         return Size(maxOf(0f, widthDiff / 2f), maxOf(0f, heightDiff / 2f))
1392     }
1393 
1394     /**
1395      * The distance within the [minimumTouchTargetSize] of [pointerPosition] to the layout size. If
1396      * [pointerPosition] isn't within [minimumTouchTargetSize], then [Float.POSITIVE_INFINITY] is
1397      * returned.
1398      */
1399     protected fun distanceInMinimumTouchTarget(
1400         pointerPosition: Offset,
1401         minimumTouchTargetSize: Size
1402     ): Float {
1403         if (
1404             measuredWidth >= minimumTouchTargetSize.width &&
1405                 measuredHeight >= minimumTouchTargetSize.height
1406         ) {
1407             // this layout is big enough that it doesn't qualify for minimum touch targets
1408             return Float.POSITIVE_INFINITY
1409         }
1410 
1411         val (width, height) = calculateMinimumTouchTargetPadding(minimumTouchTargetSize)
1412         val offsetFromEdge = offsetFromEdge(pointerPosition)
1413 
1414         return if (
1415             (width > 0f || height > 0f) && offsetFromEdge.x <= width && offsetFromEdge.y <= height
1416         ) {
1417             offsetFromEdge.getDistanceSquared()
1418         } else {
1419             Float.POSITIVE_INFINITY // miss
1420         }
1421     }
1422 
1423     /**
1424      * [LayoutNode.hitTest] and [LayoutNode.hitTestSemantics] are very similar, but the data used in
1425      * their implementations are different. This extracts the differences between the two methods
1426      * into a single interface.
1427      */
1428     internal interface HitTestSource {
1429         /** Returns the [NodeKind] for the hit test target. */
1430         fun entityType(): NodeKind<*>
1431 
1432         /**
1433          * Pointer input hit tests can intercept child hits when enabled. This returns `true` if the
1434          * modifier has requested intercepting.
1435          */
1436         fun interceptOutOfBoundsChildEvents(node: Modifier.Node): Boolean
1437 
1438         /**
1439          * Returns false if the parent layout node has a state that suppresses hit testing of its
1440          * children.
1441          */
1442         fun shouldHitTestChildren(parentLayoutNode: LayoutNode): Boolean
1443 
1444         /** Calls a hit test on [layoutNode]. */
1445         fun childHitTest(
1446             layoutNode: LayoutNode,
1447             pointerPosition: Offset,
1448             hitTestResult: HitTestResult,
1449             pointerType: PointerType,
1450             isInLayer: Boolean
1451         )
1452     }
1453 
1454     internal companion object {
1455         const val ExpectAttachedLayoutCoordinates =
1456             "LayoutCoordinate operations are only valid " + "when isAttached is true"
1457         const val UnmeasuredError = "Asking for measurement result of unmeasured layout modifier"
1458         private val onCommitAffectingLayerParams: (NodeCoordinator) -> Unit = { coordinator ->
1459             if (coordinator.isValidOwnerScope) {
1460                 // coordinator.layerPositionalProperties should always be non-null here, but
1461                 // we'll just be careful with a null check.
1462                 val positionalPropertiesChanged = coordinator.updateLayerParameters()
1463                 if (positionalPropertiesChanged) {
1464                     val layoutNode = coordinator.layoutNode
1465                     val layoutDelegate = layoutNode.layoutDelegate
1466                     if (layoutDelegate.childrenAccessingCoordinatesDuringPlacement > 0) {
1467                         if (
1468                             layoutDelegate.coordinatesAccessedDuringModifierPlacement ||
1469                                 layoutDelegate.coordinatesAccessedDuringPlacement
1470                         ) {
1471                             layoutNode.requestRelayout()
1472                         }
1473                         layoutDelegate.measurePassDelegate
1474                             .notifyChildrenUsingCoordinatesWhilePlacing()
1475                     }
1476                     val owner = layoutNode.requireOwner()
1477                     owner.rectManager.onLayoutLayerPositionalPropertiesChanged(layoutNode)
1478                     owner.requestOnPositionedCallback(layoutNode)
1479                 }
1480             }
1481         }
1482         private val onCommitAffectingLayer: (NodeCoordinator) -> Unit = { coordinator ->
1483             coordinator.layer?.invalidate()
1484         }
1485         private val graphicsLayerScope = ReusableGraphicsLayerScope()
1486         private val tmpLayerPositionalProperties = LayerPositionalProperties()
1487 
1488         // Used for matrix calculations. It should not be used for anything that could lead to
1489         // reentrancy.
1490         private val tmpMatrix = Matrix()
1491 
1492         /** Hit testing specifics for pointer input. */
1493         val PointerInputSource =
1494             object : HitTestSource {
1495                 override fun entityType() = Nodes.PointerInput
1496 
1497                 override fun interceptOutOfBoundsChildEvents(node: Modifier.Node): Boolean {
1498                     node.dispatchForKind(Nodes.PointerInput) {
1499                         if (it.interceptOutOfBoundsChildEvents()) return true
1500                     }
1501                     return false
1502                 }
1503 
1504                 override fun shouldHitTestChildren(parentLayoutNode: LayoutNode) = true
1505 
1506                 override fun childHitTest(
1507                     layoutNode: LayoutNode,
1508                     pointerPosition: Offset,
1509                     hitTestResult: HitTestResult,
1510                     pointerType: PointerType,
1511                     isInLayer: Boolean
1512                 ) = layoutNode.hitTest(pointerPosition, hitTestResult, pointerType, isInLayer)
1513             }
1514 
1515         /** Hit testing specifics for semantics. */
1516         val SemanticsSource =
1517             object : HitTestSource {
1518                 override fun entityType() = Nodes.Semantics
1519 
1520                 override fun interceptOutOfBoundsChildEvents(node: Modifier.Node) = false
1521 
1522                 override fun shouldHitTestChildren(parentLayoutNode: LayoutNode) =
1523                     parentLayoutNode.semanticsConfiguration?.isClearingSemantics != true
1524 
1525                 override fun childHitTest(
1526                     layoutNode: LayoutNode,
1527                     pointerPosition: Offset,
1528                     hitTestResult: HitTestResult,
1529                     pointerType: PointerType,
1530                     isInLayer: Boolean
1531                 ) =
1532                     layoutNode.hitTestSemantics(
1533                         pointerPosition,
1534                         hitTestResult,
1535                         pointerType,
1536                         isInLayer
1537                     )
1538             }
1539     }
1540 }
1541 
1542 @Suppress("PrimitiveInCollection")
compareEqualsnull1543 private fun compareEquals(
1544     a: MutableObjectIntMap<AlignmentLine>?,
1545     b: Map<AlignmentLine, Int>
1546 ): Boolean {
1547     if (a == null) return false
1548     if (a.size != b.size) return false
1549 
1550     a.forEach { k, v -> if (b[k] != v) return false }
1551 
1552     return true
1553 }
1554 
1555 /**
1556  * These are the components of a layer that changes the position and may lead to an
1557  * OnGloballyPositionedCallback.
1558  */
1559 private class LayerPositionalProperties {
1560     private var scaleX: Float = 1f
1561     private var scaleY: Float = 1f
1562     private var translationX: Float = 0f
1563     private var translationY: Float = 0f
1564     private var rotationX: Float = 0f
1565     private var rotationY: Float = 0f
1566     private var rotationZ: Float = 0f
1567     private var cameraDistance: Float = DefaultCameraDistance
1568     private var transformOrigin: TransformOrigin = TransformOrigin.Center
1569 
copyFromnull1570     fun copyFrom(other: LayerPositionalProperties) {
1571         scaleX = other.scaleX
1572         scaleY = other.scaleY
1573         translationX = other.translationX
1574         translationY = other.translationY
1575         rotationX = other.rotationX
1576         rotationY = other.rotationY
1577         rotationZ = other.rotationZ
1578         cameraDistance = other.cameraDistance
1579         transformOrigin = other.transformOrigin
1580     }
1581 
copyFromnull1582     fun copyFrom(scope: GraphicsLayerScope) {
1583         scaleX = scope.scaleX
1584         scaleY = scope.scaleY
1585         translationX = scope.translationX
1586         translationY = scope.translationY
1587         rotationX = scope.rotationX
1588         rotationY = scope.rotationY
1589         rotationZ = scope.rotationZ
1590         cameraDistance = scope.cameraDistance
1591         transformOrigin = scope.transformOrigin
1592     }
1593 
hasSameValuesAsnull1594     fun hasSameValuesAs(other: LayerPositionalProperties): Boolean {
1595         return scaleX == other.scaleX &&
1596             scaleY == other.scaleY &&
1597             translationX == other.translationX &&
1598             translationY == other.translationY &&
1599             rotationX == other.rotationX &&
1600             rotationY == other.rotationY &&
1601             rotationZ == other.rotationZ &&
1602             cameraDistance == other.cameraDistance &&
1603             transformOrigin == other.transformOrigin
1604     }
1605 }
1606 
nextUntilnull1607 private fun DelegatableNode.nextUntil(type: NodeKind<*>, stopType: NodeKind<*>): Modifier.Node? {
1608     val child = node.child ?: return null
1609     if (child.aggregateChildKindSet and type.mask == 0) return null
1610     var next: Modifier.Node? = child
1611     while (next != null) {
1612         val kindSet = next.kindSet
1613         if (kindSet and stopType.mask != 0) return null
1614         if (kindSet and type.mask != 0) {
1615             return next
1616         }
1617         next = next.child
1618     }
1619     return null
1620 }
1621