1 /*
<lambda>null2  * Copyright 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.ui.node
18 
19 import androidx.compose.runtime.collection.mutableVectorOf
20 import androidx.compose.ui.internal.checkPrecondition
21 import androidx.compose.ui.internal.requirePrecondition
22 import androidx.compose.ui.layout.OnGloballyPositionedModifier
23 import androidx.compose.ui.node.LayoutNode.LayoutState.Idle
24 import androidx.compose.ui.node.LayoutNode.LayoutState.LayingOut
25 import androidx.compose.ui.node.LayoutNode.LayoutState.LookaheadLayingOut
26 import androidx.compose.ui.node.LayoutNode.LayoutState.LookaheadMeasuring
27 import androidx.compose.ui.node.LayoutNode.LayoutState.Measuring
28 import androidx.compose.ui.node.LayoutNode.UsageByParent.InLayoutBlock
29 import androidx.compose.ui.node.LayoutNode.UsageByParent.InMeasureBlock
30 import androidx.compose.ui.node.RootForTest.UncaughtExceptionHandler
31 import androidx.compose.ui.unit.Constraints
32 
33 /**
34  * Keeps track of [LayoutNode]s which needs to be remeasured or relaid out.
35  *
36  * Use [requestRemeasure] to schedule remeasuring or [requestRelayout] to schedule relayout.
37  *
38  * Use [measureAndLayout] to perform scheduled actions and [dispatchOnPositionedCallbacks] to
39  * dispatch [OnGloballyPositionedModifier] callbacks for the nodes affected by the previous
40  * [measureAndLayout] execution.
41  */
42 internal class MeasureAndLayoutDelegate(private val root: LayoutNode) {
43     /** LayoutNodes that need measure or layout. */
44     private val relayoutNodes = DepthSortedSetsForDifferentPasses(Owner.enableExtraAssertions)
45 
46     /** Whether any LayoutNode needs measure or layout. */
47     val hasPendingMeasureOrLayout
48         get() = relayoutNodes.isNotEmpty()
49 
50     /** Whether any on positioned callbacks need to be dispatched */
51     val hasPendingOnPositionedCallbacks
52         get() = onPositionedDispatcher.isNotEmpty()
53 
54     /** Flag to indicate that we're currently measuring. */
55     internal var duringMeasureLayout = false
56     /**
57      * True when we are currently executing a full measure/layout pass, which mean we will iterate
58      * through all the nodes in [relayoutNodes].
59      */
60     private var duringFullMeasureLayoutPass = false
61 
62     /** Dispatches on positioned callbacks. */
63     private val onPositionedDispatcher = OnPositionedDispatcher()
64 
65     /** List of listeners that must be called after layout has completed. */
66     private val onLayoutCompletedListeners = mutableVectorOf<Owner.OnLayoutCompletedListener>()
67 
68     /**
69      * The current measure iteration. The value is incremented during the [measureAndLayout]
70      * execution. Some [measureAndLayout] executions will increment it more than once.
71      */
72     var measureIteration: Long = 1L
73         get() {
74             requirePrecondition(duringMeasureLayout) {
75                 "measureIteration should be only used during the measure/layout pass"
76             }
77             return field
78         }
79         private set
80 
81     /**
82      * Stores the list of [LayoutNode]s scheduled to be remeasured in the next measure/layout pass.
83      * We were unable to mark them as needsRemeasure=true previously as this request happened during
84      * the previous measure/layout pass and they were already measured as part of it. See
85      * [requestRemeasure] for more details.
86      */
87     private val postponedMeasureRequests = mutableVectorOf<PostponedRequest>()
88 
89     private var rootConstraints: Constraints? = null
90 
91     internal var uncaughtExceptionHandler: UncaughtExceptionHandler? = null
92 
93     /** @param constraints The constraints to measure the root [LayoutNode] with */
94     fun updateRootConstraints(constraints: Constraints) {
95         if (rootConstraints != constraints) {
96             requirePrecondition(!duringMeasureLayout) {
97                 "updateRootConstraints called while measuring"
98             }
99             rootConstraints = constraints
100             if (root.lookaheadRoot != null) {
101                 root.markLookaheadMeasurePending()
102             }
103             root.markMeasurePending()
104             relayoutNodes.add(root, root.lookaheadRoot != null)
105         }
106     }
107 
108     private val consistencyChecker: LayoutTreeConsistencyChecker? =
109         if (Owner.enableExtraAssertions) {
110             LayoutTreeConsistencyChecker(
111                 root,
112                 relayoutNodes,
113                 postponedMeasureRequests.asMutableList(),
114             )
115         } else {
116             null
117         }
118 
119     /**
120      * Requests lookahead remeasure for this [layoutNode] and nodes affected by its measure result
121      *
122      * Note: This should only be called on a [LayoutNode] in the subtree defined in a
123      * LookaheadScope. The caller is responsible for checking with [LayoutNode.lookaheadRoot] is
124      * valid (i.e. non-null) before calling this method.
125      *
126      * @return true if the [measureAndLayout] execution should be scheduled as a result of the
127      *   request.
128      */
129     fun requestLookaheadRemeasure(layoutNode: LayoutNode, forced: Boolean = false): Boolean {
130         checkPrecondition(layoutNode.lookaheadRoot != null) {
131             "Error: requestLookaheadRemeasure cannot be called on a node outside" +
132                 " LookaheadScope"
133         }
134         return when (layoutNode.layoutState) {
135             LookaheadMeasuring -> {
136                 // requestLookaheadRemeasure has already been called for this node or
137                 // we're currently measuring it, let's swallow.
138                 false
139             }
140             Measuring,
141             LookaheadLayingOut,
142             LayingOut -> {
143                 // requestLookaheadRemeasure is currently laying out and it is incorrect to
144                 // request lookahead remeasure now, let's postpone it.
145                 postponedMeasureRequests.add(
146                     PostponedRequest(node = layoutNode, isLookahead = true, isForced = forced)
147                 )
148                 consistencyChecker?.assertConsistent()
149                 false
150             }
151             Idle -> {
152                 if (layoutNode.lookaheadMeasurePending && !forced) {
153                     false
154                 } else {
155                     layoutNode.markLookaheadMeasurePending()
156                     layoutNode.markMeasurePending()
157                     // for the deactivated nodes we want to mark them as dirty, but not to trigger
158                     // measureAndLayout() pass as they will be skipped.
159                     if (layoutNode.isDeactivated) {
160                         false
161                     } else {
162                         if (
163                             (layoutNode.isPlacedInLookahead == true ||
164                                 layoutNode.canAffectParentInLookahead) &&
165                                 layoutNode.parent?.lookaheadMeasurePending != true
166                         ) {
167                             relayoutNodes.add(layoutNode, true)
168                         } else if (
169                             (layoutNode.isPlaced || layoutNode.canAffectPlacedParent) &&
170                                 layoutNode.parent?.measurePending != true
171                         ) {
172                             relayoutNodes.add(layoutNode, false)
173                         }
174                         !duringFullMeasureLayoutPass
175                     }
176                 }
177             }
178         }
179     }
180 
181     /**
182      * Requests remeasure for this [layoutNode] and nodes affected by its measure result.
183      *
184      * @return true if the [measureAndLayout] execution should be scheduled as a result of the
185      *   request.
186      */
187     fun requestRemeasure(layoutNode: LayoutNode, forced: Boolean = false): Boolean =
188         when (layoutNode.layoutState) {
189             Measuring,
190             LookaheadMeasuring -> {
191                 // requestMeasure has already been called for this node or
192                 // we're currently measuring it, let's swallow. example when it happens: we compose
193                 // DataNode inside BoxWithConstraints, this calls onRequestMeasure on DataNode's
194                 // parent, but this parent is BoxWithConstraints which is currently measuring.
195                 false
196             }
197             LookaheadLayingOut,
198             LayingOut -> {
199                 // requestMeasure is currently laying out and it is incorrect to request remeasure
200                 // now, let's postpone it.
201                 postponedMeasureRequests.add(
202                     PostponedRequest(node = layoutNode, isLookahead = false, isForced = forced)
203                 )
204                 consistencyChecker?.assertConsistent()
205                 false
206             }
207             Idle -> {
208                 if (layoutNode.measurePending && !forced) {
209                     false
210                 } else {
211                     layoutNode.markMeasurePending()
212                     // for the deactivated nodes we want to mark them as dirty, but not to trigger
213                     // measureAndLayout() pass as they will be skipped.
214                     if (layoutNode.isDeactivated) {
215                         false
216                     } else {
217                         if (layoutNode.isPlaced || layoutNode.canAffectPlacedParent) {
218                             if (layoutNode.parent?.measurePending != true) {
219                                 relayoutNodes.add(layoutNode, false)
220                             }
221                             !duringFullMeasureLayoutPass
222                         } else {
223                             false // it can't affect parent
224                         }
225                     }
226                 }
227             }
228         }
229 
230     /**
231      * Requests lookahead relayout for this [layoutNode] and nodes affected by its position.
232      *
233      * @return true if the [measureAndLayout] execution should be scheduled as a result of the
234      *   request.
235      */
236     fun requestLookaheadRelayout(layoutNode: LayoutNode, forced: Boolean = false): Boolean =
237         when (layoutNode.layoutState) {
238             LookaheadMeasuring,
239             LookaheadLayingOut -> {
240                 // Don't need to do anything else since the parent is already scheduled
241                 // for a lookahead relayout (lookahead measure will trigger lookahead
242                 // relayout), or lookahead layout is in process right now
243                 consistencyChecker?.assertConsistent()
244                 false
245             }
246             Measuring,
247             LayingOut,
248             Idle -> {
249                 if (
250                     (layoutNode.lookaheadMeasurePending || layoutNode.lookaheadLayoutPending) &&
251                         !forced
252                 ) {
253                     // Don't need to do anything else since the parent is already scheduled
254                     // for a lookahead relayout (lookahead measure will trigger lookahead
255                     // relayout)
256                     consistencyChecker?.assertConsistent()
257                     false
258                 } else {
259                     // Mark both lookahead layout and layout as pending, as layout has a
260                     // dependency on lookahead layout.
261                     layoutNode.markLookaheadLayoutPending()
262                     layoutNode.markLayoutPending()
263                     // for the deactivated nodes we want to mark them as dirty, but not to trigger
264                     // measureAndLayout() pass as they will be skipped.
265                     if (layoutNode.isDeactivated) {
266                         false
267                     } else {
268                         val parent = layoutNode.parent
269                         if (
270                             layoutNode.isPlacedInLookahead == true &&
271                                 parent?.lookaheadMeasurePending != true &&
272                                 parent?.lookaheadLayoutPending != true
273                         ) {
274                             relayoutNodes.add(layoutNode, true)
275                         } else if (
276                             layoutNode.isPlaced &&
277                                 parent?.layoutPending != true &&
278                                 parent?.measurePending != true
279                         ) {
280                             relayoutNodes.add(layoutNode, false)
281                         }
282                         !duringFullMeasureLayoutPass
283                     }
284                 }
285             }
286         }
287 
288     /**
289      * Requests relayout for this [layoutNode] and nodes affected by its position.
290      *
291      * @return true if the [measureAndLayout] execution should be scheduled as a result of the
292      *   request.
293      */
294     fun requestRelayout(layoutNode: LayoutNode, forced: Boolean = false): Boolean =
295         when (layoutNode.layoutState) {
296             Measuring,
297             LookaheadMeasuring,
298             LookaheadLayingOut,
299             LayingOut -> {
300                 // don't need to do anything else since the parent is already scheduled
301                 // for a relayout (measure will trigger relayout), or is laying out right now
302                 consistencyChecker?.assertConsistent()
303                 false
304             }
305             Idle -> {
306                 val parent = layoutNode.parent
307                 val parentIsPlaced = parent == null || parent.isPlaced
308                 if (
309                     !forced &&
310                         (layoutNode.measurePending ||
311                             (layoutNode.layoutPending &&
312                                 layoutNode.isPlaced == parentIsPlaced &&
313                                 layoutNode.isPlaced == layoutNode.isPlacedByParent))
314                 ) {
315                     // don't need to do anything else since the parent is already scheduled
316                     // for a relayout (measure will trigger relayout), or is laying out right now
317                     consistencyChecker?.assertConsistent()
318                     false
319                 } else {
320                     layoutNode.markLayoutPending()
321                     // for the deactivated nodes we want to mark them as dirty, but not to trigger
322                     // measureAndLayout() pass as they will be skipped.
323                     if (layoutNode.isDeactivated) {
324                         false
325                     } else {
326                         if (layoutNode.isPlacedByParent && parentIsPlaced) {
327                             if (parent?.layoutPending != true && parent?.measurePending != true) {
328                                 relayoutNodes.add(layoutNode, false)
329                             }
330                             !duringFullMeasureLayoutPass
331                         } else {
332                             false // the node can't affect parent
333                         }
334                     }
335                 }
336             }
337         }
338 
339     /** Request that [layoutNode] and children should call their position change callbacks. */
340     fun requestOnPositionedCallback(layoutNode: LayoutNode) {
341         onPositionedDispatcher.onNodePositioned(layoutNode)
342     }
343 
344     /** @return true if the [LayoutNode] size has been changed. */
345     private fun doLookaheadRemeasure(layoutNode: LayoutNode, constraints: Constraints?): Boolean {
346         if (layoutNode.lookaheadRoot == null) return false
347         val lookaheadSizeChanged =
348             if (constraints != null) {
349                 layoutNode.lookaheadRemeasure(constraints)
350             } else {
351                 layoutNode.lookaheadRemeasure()
352             }
353 
354         val parent = layoutNode.parent
355         if (lookaheadSizeChanged && parent != null) {
356             if (parent.lookaheadRoot == null) {
357                 parent.requestRemeasure(invalidateIntrinsics = false)
358             } else if (layoutNode.measuredByParentInLookahead == InMeasureBlock) {
359                 parent.requestLookaheadRemeasure(invalidateIntrinsics = false)
360             } else if (layoutNode.measuredByParentInLookahead == InLayoutBlock) {
361                 parent.requestLookaheadRelayout()
362             }
363         }
364         return lookaheadSizeChanged
365     }
366 
367     private fun doRemeasure(layoutNode: LayoutNode, constraints: Constraints?): Boolean {
368         val sizeChanged =
369             if (constraints != null) {
370                 layoutNode.remeasure(constraints)
371             } else {
372                 layoutNode.remeasure()
373             }
374         val parent = layoutNode.parent
375         if (sizeChanged && parent != null) {
376             if (layoutNode.measuredByParent == InMeasureBlock) {
377                 parent.requestRemeasure(invalidateIntrinsics = false)
378             } else if (layoutNode.measuredByParent == InLayoutBlock) {
379                 parent.requestRelayout()
380             }
381         }
382         return sizeChanged
383     }
384 
385     /**
386      * Iterates through all LayoutNodes that have requested layout and measures and lays them out
387      */
388     fun measureAndLayout(onLayout: (() -> Unit)? = null): Boolean {
389         var rootNodeResized = false
390         performMeasureAndLayout(fullPass = true) {
391             if (relayoutNodes.isNotEmpty()) {
392                 relayoutNodes.popEach { layoutNode, affectsLookahead ->
393                     val sizeChanged = remeasureAndRelayoutIfNeeded(layoutNode, affectsLookahead)
394                     if (layoutNode === root && sizeChanged) {
395                         rootNodeResized = true
396                     }
397                 }
398                 onLayout?.invoke()
399             }
400         }
401         callOnLayoutCompletedListeners()
402         return rootNodeResized
403     }
404 
405     /**
406      * Only does measurement from the root without doing any placement. This is intended to be
407      * called to determine only how large the root is with minimal effort.
408      */
409     fun measureOnly() {
410         if (relayoutNodes.isNotEmpty()) {
411             performMeasureAndLayout(fullPass = false) {
412                 if (!relayoutNodes.isEmpty(affectsLookahead = true)) {
413                     if (root.lookaheadRoot != null) {
414                         // This call will walk the tree to look for lookaheadMeasurePending nodes
415                         // and
416                         // do a lookahead remeasure for those nodes only.
417                         remeasureOnly(root, affectsLookahead = true)
418                     } else {
419                         // First do a lookahead remeasure pass for all the lookaheadMeasurePending
420                         // nodes,
421                         // followed by a remeasure pass for the rest of the tree.
422                         remeasureLookaheadRootsInSubtree(root)
423                     }
424                 }
425                 remeasureOnly(root, affectsLookahead = false)
426             }
427         }
428     }
429 
430     private fun remeasureLookaheadRootsInSubtree(layoutNode: LayoutNode) {
431         layoutNode.forEachChild {
432             if (it.measureAffectsParent) {
433                 if (it.isOutMostLookaheadRoot) {
434                     // This call will walk the subtree to look for lookaheadMeasurePending nodes and
435                     // do a recursive lookahead remeasure starting at the root.
436                     remeasureOnly(it, affectsLookahead = true)
437                 } else {
438                     // Only search downward when no lookahead root is found
439                     remeasureLookaheadRootsInSubtree(it)
440                 }
441             }
442         }
443     }
444 
445     fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {
446         if (layoutNode.isDeactivated) {
447             // regular measureAndLayout() pass will skip deactivated nodes, so here we should
448             // do nothing as well.
449             return
450         }
451         requirePrecondition(layoutNode != root) { "measureAndLayout called on root" }
452         performMeasureAndLayout(fullPass = false) {
453             relayoutNodes.remove(layoutNode)
454             // we don't check for the layoutState as even if the node doesn't need remeasure
455             // it could be remeasured because the constraints changed.
456             val lookaheadSizeChanged = doLookaheadRemeasure(layoutNode, constraints)
457             if (
458                 (lookaheadSizeChanged || layoutNode.lookaheadLayoutPending) &&
459                     layoutNode.isPlacedInLookahead == true
460             ) {
461                 layoutNode.lookaheadReplace()
462             }
463             // Make sure the subtree starting from [layoutNode] are lookahead replaced. This is
464             // needed because the child nodes that are skipped in `lookaheadReplace` above
465             // due to not changing position may not be skipped by the `replace` call below. Hence
466             // we can avoid having `replace` called for nodes that have not been lookahead placed.
467             ensureSubtreeLookaheadReplaced(layoutNode)
468 
469             doRemeasure(layoutNode, constraints)
470             if (layoutNode.layoutPending && layoutNode.isPlaced) {
471                 layoutNode.replace()
472                 onPositionedDispatcher.onNodePositioned(layoutNode)
473             }
474 
475             drainPostponedMeasureRequests()
476         }
477         callOnLayoutCompletedListeners()
478     }
479 
480     private fun ensureSubtreeLookaheadReplaced(layoutNode: LayoutNode) {
481         layoutNode.forEachChild {
482             if (it.isPlacedInLookahead == true && !it.isDeactivated) {
483                 if (relayoutNodes.contains(it, true)) {
484                     // Only replace if invalidation pending
485                     it.lookaheadReplace()
486                 }
487                 ensureSubtreeLookaheadReplaced(it)
488             }
489         }
490     }
491 
492     private inline fun performMeasureAndLayout(fullPass: Boolean, block: () -> Unit) {
493         requirePrecondition(root.isAttached) {
494             "performMeasureAndLayout called with unattached root"
495         }
496         requirePrecondition(root.isPlaced) { "performMeasureAndLayout called with unplaced root" }
497         requirePrecondition(!duringMeasureLayout) {
498             "performMeasureAndLayout called during measure layout"
499         }
500         // we don't need to measure any children unless we have the correct root constraints
501         if (rootConstraints != null) {
502             duringMeasureLayout = true
503             duringFullMeasureLayoutPass = fullPass
504             try {
505                 block()
506             } catch (t: Throwable) {
507                 uncaughtExceptionHandler?.onUncaughtException(t) ?: throw t
508             } finally {
509                 duringMeasureLayout = false
510                 duringFullMeasureLayoutPass = false
511             }
512             consistencyChecker?.assertConsistent()
513         }
514     }
515 
516     fun registerOnLayoutCompletedListener(listener: Owner.OnLayoutCompletedListener) {
517         onLayoutCompletedListeners += listener
518     }
519 
520     private fun callOnLayoutCompletedListeners() {
521         onLayoutCompletedListeners.forEach { it.onLayoutComplete() }
522         onLayoutCompletedListeners.clear()
523     }
524 
525     /**
526      * Does actual remeasure and relayout on the node if it is required. The [layoutNode] should be
527      * already removed from [relayoutNodes] before running it.
528      *
529      * When [affectsLookahead] is false, we'll skip lookahead measure & layout, and only measure and
530      * layout as needed. This is needed because we don't want [forceMeasureTheSubtree] that doesn't
531      * affect lookahead to leak into lookahead and start doing lookahead measure/layout. That would
532      * prevent some of the lookahead remeasure/relayout requests from being properly handled as the
533      * starting node of [forceMeasureTheSubtree] would be in [LayoutNode.LayoutState.Measuring]
534      * until it returns.
535      *
536      * Note, when [affectsLookahead] is true, we will only do lookahead measure and layout.
537      *
538      * @return true if the [LayoutNode] size has been changed.
539      */
540     private fun remeasureAndRelayoutIfNeeded(
541         layoutNode: LayoutNode,
542         affectsLookahead: Boolean = true,
543         relayoutNeeded: Boolean = true
544     ): Boolean {
545         var sizeChanged = false
546         if (layoutNode.isDeactivated) {
547             // we don't remeasure or relayout deactivated nodes.
548             return false
549         }
550         if (
551             layoutNode.isPlaced || // the root node doesn't have isPlacedByParent = true
552                 layoutNode.isPlacedByParent ||
553                 layoutNode.canAffectPlacedParent ||
554                 layoutNode.isPlacedInLookahead == true ||
555                 layoutNode.canAffectParentInLookahead ||
556                 layoutNode.alignmentLinesRequired
557         ) {
558             val constraints = if (layoutNode === root) rootConstraints!! else null
559             if (affectsLookahead) {
560                 // Only do lookahead invalidation when affectsLookahead is true
561                 if (layoutNode.lookaheadMeasurePending) {
562                     sizeChanged = doLookaheadRemeasure(layoutNode, constraints)
563                 }
564                 if (relayoutNeeded) {
565                     if (
566                         (sizeChanged || layoutNode.lookaheadLayoutPending) &&
567                             layoutNode.isPlacedInLookahead == true
568                     ) {
569                         layoutNode.lookaheadReplace()
570                     }
571                 }
572             } else {
573                 if (layoutNode.measurePending) {
574                     sizeChanged = doRemeasure(layoutNode, constraints)
575                 }
576                 if (relayoutNeeded) {
577                     if (layoutNode.layoutPending) {
578                         val isPlacedByPlacedParent =
579                             layoutNode === root ||
580                                 (layoutNode.parent?.isPlaced == true && layoutNode.isPlacedByParent)
581                         if (isPlacedByPlacedParent) {
582                             if (layoutNode === root) {
583                                 layoutNode.place(0, 0)
584                             } else {
585                                 layoutNode.replace()
586                             }
587                             onPositionedDispatcher.onNodePositioned(layoutNode)
588                             // Since there has been an update to a coordinator somewhere in the
589                             // modifier chain of this layout node, we might have onRectChanged
590                             // callbacks that need to be notified of that change. As a result, even
591                             // if the outer rect of this layout node hasn't changed, we want to
592                             // invalidate the callbacks for them
593                             layoutNode.requireOwner().rectManager.invalidateCallbacksFor(layoutNode)
594                             consistencyChecker?.assertConsistent()
595                         }
596                     }
597                 }
598             }
599             drainPostponedMeasureRequests()
600         }
601         return sizeChanged
602     }
603 
604     private fun drainPostponedMeasureRequests() {
605         if (postponedMeasureRequests.isNotEmpty()) {
606             postponedMeasureRequests.forEach { request ->
607                 if (request.node.isAttached) {
608                     if (!request.isLookahead) {
609                         request.node.requestRemeasure(
610                             forceRequest = request.isForced,
611                             invalidateIntrinsics = false
612                         )
613                     } else {
614                         request.node.requestLookaheadRemeasure(
615                             forceRequest = request.isForced,
616                             invalidateIntrinsics = false
617                         )
618                     }
619                 }
620             }
621             postponedMeasureRequests.clear()
622         }
623     }
624 
625     /**
626      * Remeasures [layoutNode] if it has [LayoutNode.measurePending] or
627      * [LayoutNode.lookaheadMeasurePending].
628      */
629     private fun remeasureOnly(layoutNode: LayoutNode, affectsLookahead: Boolean) {
630         if (layoutNode.isDeactivated) {
631             return
632         }
633         val constraints = if (layoutNode === root) rootConstraints!! else null
634         if (affectsLookahead) {
635             doLookaheadRemeasure(layoutNode, constraints)
636         } else {
637             doRemeasure(layoutNode, constraints)
638         }
639     }
640 
641     /**
642      * Makes sure the passed [layoutNode] and its subtree has the final sizes. The nodes which can
643      * potentially affect the parent size will be remeasured.
644      *
645      * The node or some of the nodes in its subtree can still be kept unmeasured if they are not
646      * placed and don't affect the parent size. See [requestRemeasure] for details.
647      */
648     fun forceMeasureTheSubtree(layoutNode: LayoutNode, affectsLookahead: Boolean) {
649         // assert that it is executed during the `measureAndLayout` pass.
650         checkPrecondition(duringMeasureLayout) {
651             "forceMeasureTheSubtree should be executed during the measureAndLayout pass"
652         }
653 
654         // if this node is not yet measured this invocation shouldn't be needed.
655         requirePrecondition(!layoutNode.measurePending(affectsLookahead)) {
656             "node not yet measured"
657         }
658 
659         forceMeasureTheSubtreeInternal(layoutNode, affectsLookahead)
660     }
661 
662     private fun onlyRemeasureIfPending(node: LayoutNode, affectsLookahead: Boolean) {
663         if (node.measurePending(affectsLookahead)) {
664             // we don't need to run relayout as part of this logic. so the node will
665             // not be removed from `relayoutNodes` in order to be visited again during
666             // the regular pass. it is important as the parent of this node can decide
667             // to not place this child, so the child relayout should be skipped.
668             remeasureAndRelayoutIfNeeded(node, affectsLookahead, relayoutNeeded = false)
669         }
670     }
671 
672     private fun forceMeasureTheSubtreeInternal(layoutNode: LayoutNode, affectsLookahead: Boolean) {
673         layoutNode.forEachChild { child ->
674             // only proceed if child's size can affect the parent size
675             if (
676                 !affectsLookahead && child.measureAffectsParent ||
677                     affectsLookahead && child.measureAffectsParentLookahead
678             ) {
679                 // When LookaheadRoot's parent gets forceMeasureSubtree call, we need to check
680                 // both lookahead invalidation and non-lookahead invalidation, just like a measure()
681                 // call from LookaheadRoot's parent would start the two tracks - lookahead and post
682                 // lookahead measurements.
683                 if (child.isOutMostLookaheadRoot && !affectsLookahead) {
684                     // Force subtree measure hitting a lookahead root, pending lookahead measure.
685                     // This could happen when the "applyChanges" cause nodes to be attached in
686                     // lookahead subtree while the "applyChanges" is a part of the ancestor's
687                     // subcomposition in the measure pass.
688                     if (child.lookaheadMeasurePending && relayoutNodes.contains(child, true)) {
689                         remeasureAndRelayoutIfNeeded(child, true, relayoutNeeded = false)
690                     } else {
691                         forceMeasureTheSubtree(child, true)
692                     }
693                 }
694 
695                 onlyRemeasureIfPending(child, affectsLookahead)
696 
697                 // if the child is still in NeedsRemeasure state then this child remeasure wasn't
698                 // needed. it can happen for example when this child is not placed and can't affect
699                 // the parent size. we can skip the whole subtree.
700                 if (!child.measurePending(affectsLookahead)) {
701                     // run recursively for the subtree.
702                     forceMeasureTheSubtreeInternal(child, affectsLookahead)
703                 }
704             }
705         }
706 
707         // if the child was resized during the remeasurement it could request a remeasure on
708         // the parent. we need to remeasure now as this function assumes the whole subtree is
709         // fully measured as a result of the invocation.
710         onlyRemeasureIfPending(layoutNode, affectsLookahead)
711     }
712 
713     /**
714      * Dispatch [OnPositionedModifier] callbacks for the nodes affected by the previous
715      * [measureAndLayout] execution.
716      *
717      * @param forceDispatch true means the whole tree should dispatch the callback (for example when
718      *   the global position of the Owner has been changed)
719      */
720     fun dispatchOnPositionedCallbacks(forceDispatch: Boolean = false) {
721         if (forceDispatch) {
722             onPositionedDispatcher.onRootNodePositioned(root)
723         }
724         onPositionedDispatcher.dispatch()
725     }
726 
727     /**
728      * Removes [node] from the list of LayoutNodes being scheduled for the remeasure/relayout as it
729      * was detached.
730      */
731     fun onNodeDetached(node: LayoutNode) {
732         relayoutNodes.remove(node)
733         onPositionedDispatcher.remove(node)
734     }
735 
736     private val LayoutNode.measureAffectsParent
737         get() =
738             (measuredByParent == InMeasureBlock ||
739                 layoutDelegate.alignmentLinesOwner.alignmentLines.required)
740 
741     /** Checks if there is a placed parent which size we can theoretically affect by remeasuring. */
742     private val LayoutNode.measureAffectsPlacedParent: Boolean
743         get() {
744             var node = this
745             while (
746                 node.measureAffectsParent ||
747                     // if the parent is currently measuring, then measuredByParent on children was
748                     // reset to NotUsed beforehand and does not represent the real usage.
749                     node.parent?.layoutState == Measuring
750             ) {
751                 val parent = node.parent ?: return false
752                 if (parent.isPlaced) {
753                     return true
754                 }
755                 node = parent
756             }
757             return false
758         }
759 
760     private val LayoutNode.canAffectPlacedParent
761         get() = measurePending && measureAffectsPlacedParent
762 
763     private val LayoutNode.canAffectParentInLookahead
764         get() = lookaheadMeasurePending && measureAffectsParentLookahead
765 
766     private val LayoutNode.measureAffectsParentLookahead
767         get() =
768             (measuredByParentInLookahead == InMeasureBlock ||
769                 layoutDelegate.lookaheadAlignmentLinesOwner?.alignmentLines?.required == true)
770 
771     private fun LayoutNode.measurePending(affectsLookahead: Boolean) =
772         if (affectsLookahead) lookaheadMeasurePending else measurePending
773 
774     class PostponedRequest(val node: LayoutNode, val isLookahead: Boolean, val isForced: Boolean)
775 }
776