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.inspection.inspector
18 
19 import android.view.View
20 import android.view.ViewGroup
21 import android.view.inspector.WindowInspector
22 import android.widget.TextView
23 import androidx.compose.animation.Crossfade
24 import androidx.compose.foundation.Image
25 import androidx.compose.foundation.background
26 import androidx.compose.foundation.layout.Arrangement
27 import androidx.compose.foundation.layout.Column
28 import androidx.compose.foundation.layout.Row
29 import androidx.compose.foundation.layout.Spacer
30 import androidx.compose.foundation.layout.fillMaxSize
31 import androidx.compose.foundation.layout.height
32 import androidx.compose.foundation.layout.heightIn
33 import androidx.compose.foundation.layout.padding
34 import androidx.compose.foundation.lazy.LazyColumn
35 import androidx.compose.foundation.text.BasicText
36 import androidx.compose.material.AlertDialog
37 import androidx.compose.material.Button
38 import androidx.compose.material.Icon
39 import androidx.compose.material.LinearProgressIndicator
40 import androidx.compose.material.MaterialTheme
41 import androidx.compose.material.ModalDrawer
42 import androidx.compose.material.Scaffold
43 import androidx.compose.material.Surface
44 import androidx.compose.material.Text
45 import androidx.compose.material.icons.Icons
46 import androidx.compose.material.icons.filled.Call
47 import androidx.compose.material.icons.filled.FavoriteBorder
48 import androidx.compose.runtime.Composable
49 import androidx.compose.runtime.CompositionLocalProvider
50 import androidx.compose.runtime.InternalComposeApi
51 import androidx.compose.runtime.currentComposer
52 import androidx.compose.runtime.getValue
53 import androidx.compose.runtime.mutableStateOf
54 import androidx.compose.runtime.remember
55 import androidx.compose.runtime.rememberCompositionContext
56 import androidx.compose.runtime.setValue
57 import androidx.compose.runtime.tooling.CompositionData
58 import androidx.compose.runtime.tooling.LocalInspectionTables
59 import androidx.compose.ui.Alignment
60 import androidx.compose.ui.Modifier
61 import androidx.compose.ui.R
62 import androidx.compose.ui.graphics.Color
63 import androidx.compose.ui.graphics.graphicsLayer
64 import androidx.compose.ui.inspection.compose.flatten
65 import androidx.compose.ui.inspection.testdata.TestActivity
66 import androidx.compose.ui.inspection.util.ThreadUtils
67 import androidx.compose.ui.layout.GraphicLayerInfo
68 import androidx.compose.ui.node.Ref
69 import androidx.compose.ui.platform.LocalDensity
70 import androidx.compose.ui.platform.LocalInspectionMode
71 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
72 import androidx.compose.ui.platform.testTag
73 import androidx.compose.ui.semantics.clearAndSetSemantics
74 import androidx.compose.ui.semantics.semantics
75 import androidx.compose.ui.semantics.text
76 import androidx.compose.ui.test.junit4.createAndroidComposeRule
77 import androidx.compose.ui.test.onNodeWithTag
78 import androidx.compose.ui.test.onNodeWithText
79 import androidx.compose.ui.test.performClick
80 import androidx.compose.ui.test.performScrollToIndex
81 import androidx.compose.ui.text.AnnotatedString
82 import androidx.compose.ui.text.TextStyle
83 import androidx.compose.ui.text.font.Font
84 import androidx.compose.ui.text.font.toFontFamily
85 import androidx.compose.ui.text.style.TextDecoration
86 import androidx.compose.ui.tooling.data.Group
87 import androidx.compose.ui.tooling.data.UiToolingDataApi
88 import androidx.compose.ui.tooling.data.asTree
89 import androidx.compose.ui.tooling.data.position
90 import androidx.compose.ui.unit.Density
91 import androidx.compose.ui.unit.Dp
92 import androidx.compose.ui.unit.TextUnit
93 import androidx.compose.ui.unit.dp
94 import androidx.compose.ui.unit.sp
95 import androidx.compose.ui.viewinterop.AndroidView
96 import androidx.compose.ui.window.Popup
97 import androidx.test.ext.junit.runners.AndroidJUnit4
98 import androidx.test.filters.LargeTest
99 import androidx.test.filters.SdkSuppress
100 import com.google.common.truth.Truth.assertThat
101 import com.google.common.truth.Truth.assertWithMessage
102 import java.util.Collections
103 import java.util.WeakHashMap
104 import kotlin.math.roundToInt
105 import org.junit.After
106 import org.junit.Before
107 import org.junit.Rule
108 import org.junit.Test
109 import org.junit.runner.RunWith
110 
111 private const val DEBUG = false
112 private const val ROOT_ID = 3L
113 private const val MAX_RECURSIONS = 2
114 private const val MAX_ITERABLE_SIZE = 5
115 
116 @LargeTest
117 @RunWith(AndroidJUnit4::class)
118 @SdkSuppress(minSdkVersion = 29) // Render id is not returned for api < 29
119 @OptIn(UiToolingDataApi::class)
120 class LayoutInspectorTreeTest {
121     private lateinit var density: Density
122 
123     @get:Rule val composeTestRule = createAndroidComposeRule<TestActivity>()
124 
125     private val fontFamily = Font(androidx.testutils.fonts.R.font.sample_font).toFontFamily()
126 
127     @Before
128     fun before() {
129         composeTestRule.activityRule.scenario.onActivity { density = Density(it) }
130         isDebugInspectorInfoEnabled = true
131     }
132 
133     private fun findAndroidComposeView(): View {
134         return findAllAndroidComposeViews().single()
135     }
136 
137     private fun findAllAndroidComposeViews(): List<View> = findAllViews("AndroidComposeView")
138 
139     private fun findAllViews(className: String): List<View> {
140         val views = mutableListOf<View>()
141         WindowInspector.getGlobalWindowViews().forEach {
142             collectAllViews(it.rootView, className, views)
143         }
144         return views
145     }
146 
147     private fun collectAllViews(view: View, className: String, views: MutableList<View>) {
148         if (view.javaClass.simpleName == className) {
149             views.add(view)
150         }
151         if (view !is ViewGroup) {
152             return
153         }
154         for (i in 0 until view.childCount) {
155             collectAllViews(view.getChildAt(i), className, views)
156         }
157     }
158 
159     @After
160     fun after() {
161         isDebugInspectorInfoEnabled = false
162     }
163 
164     @Test
165     fun doNotCommitWithDebugSetToTrue() {
166         assertThat(DEBUG).isFalse()
167     }
168 
169     @Test // regression test for b/383639244
170     fun noViews() {
171         val builder = LayoutInspectorTree()
172         builder.convert(emptyList())
173     }
174 
175     @Test
176     fun buildTree() {
177         val slotTableRecord = CompositionDataRecord.create()
178         val localDensity = Density(density = 1f, fontScale = 1f)
179         show {
180             Inspectable(slotTableRecord) {
181                 CompositionLocalProvider(LocalDensity provides localDensity) {
182                     Column {
183                         // width: 100.dp, height: 10.dp
184                         Text(
185                             text = "helloworld",
186                             color = Color.Green,
187                             fontSize = 10.sp,
188                             lineHeight = 10.sp,
189                             fontFamily = fontFamily
190                         )
191                         // width: 24.dp, height: 24.dp
192                         Icon(Icons.Filled.FavoriteBorder, null)
193                         Surface {
194                             // minwidth: 64.dp, height: 42.dp
195                             Button(onClick = {}) {
196                                 // width: 20.dp, height: 10.dp
197                                 Text(
198                                     text = "ok",
199                                     fontSize = 10.sp,
200                                     lineHeight = 10.sp,
201                                     fontFamily = fontFamily
202                                 )
203                             }
204                         }
205                     }
206                 }
207             }
208         }
209 
210         // TODO: Find out if we can set "settings put global debug_view_attributes 1" in tests
211         val view = findAndroidComposeView()
212         view.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
213         val builder = LayoutInspectorTree()
214         builder.includeAllParameters = true
215         val nodes = builder.convert(view)
216         dumpNodes(nodes, view, builder)
217 
218         validate(nodes, builder, density = localDensity) {
219             node(
220                 name = "Column",
221                 fileName = "LayoutInspectorTreeTest.kt",
222                 left = 0.0.dp,
223                 top = 0.0.dp,
224                 width = 100.dp,
225                 height = 82.dp,
226                 children = listOf("Text", "Icon", "Surface"),
227                 inlined = true,
228             )
229             node(
230                 name = "Text",
231                 isRenderNode = false,
232                 fileName = "LayoutInspectorTreeTest.kt",
233                 left = 0.dp,
234                 top = 0.0.dp,
235                 width = 100.dp,
236                 height = 10.dp,
237             )
238             node(
239                 name = "Icon",
240                 isRenderNode = true,
241                 fileName = "LayoutInspectorTreeTest.kt",
242                 left = 0.dp,
243                 top = 10.dp,
244                 width = 24.dp,
245                 height = 24.dp,
246             )
247             node(
248                 name = "Surface",
249                 fileName = "LayoutInspectorTreeTest.kt",
250                 isRenderNode = true,
251                 left = 0.dp,
252                 top = 34.dp,
253                 width = 64.dp,
254                 height = 48.dp,
255                 children = listOf("Button")
256             )
257             node(
258                 name = "Button",
259                 fileName = "LayoutInspectorTreeTest.kt",
260                 isRenderNode = true,
261                 left = 0.dp,
262                 top = 40.dp,
263                 width = 64.dp,
264                 height = 36.dp,
265                 children = listOf("Text")
266             )
267             node(
268                 name = "Text",
269                 isRenderNode = false,
270                 fileName = "LayoutInspectorTreeTest.kt",
271                 left = 21.dp,
272                 top = 53.dp,
273                 width = 23.dp,
274                 height = 10.dp,
275             )
276         }
277     }
278 
279     @Test
280     fun buildTreeWithTransformedText() {
281         val slotTableRecord = CompositionDataRecord.create()
282         val localDensity = Density(density = 1f, fontScale = 1f)
283         show {
284             Inspectable(slotTableRecord) {
285                 CompositionLocalProvider(LocalDensity provides localDensity) {
286                     Column {
287                         Text(
288                             text = "helloworld",
289                             fontSize = 10.sp,
290                             fontFamily = fontFamily,
291                             modifier = Modifier.graphicsLayer(rotationZ = -90f)
292                         )
293                     }
294                 }
295             }
296         }
297 
298         // TODO: Find out if we can set "settings put global debug_view_attributes 1" in tests
299         val view = findAndroidComposeView()
300         view.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
301         val builder = LayoutInspectorTree()
302         val nodes = builder.convert(view)
303         dumpNodes(nodes, view, builder)
304 
305         validate(nodes, builder, density = localDensity) {
306             node(
307                 name = "Column",
308                 hasTransformations = false,
309                 fileName = "LayoutInspectorTreeTest.kt",
310                 left = 0.dp,
311                 top = 0.dp,
312                 width = 100.dp,
313                 height = 10.dp,
314                 children = listOf("Text"),
315                 inlined = true,
316             )
317             node(
318                 name = "Text",
319                 isRenderNode = true,
320                 hasTransformations = true,
321                 fileName = "LayoutInspectorTreeTest.kt",
322                 left = 45.dp,
323                 top = 55.dp,
324                 width = 100.dp,
325                 height = 10.dp,
326             )
327         }
328     }
329 
330     @Test
331     fun testStitchTreeFromModelDrawerLayout() {
332         val slotTableRecord = CompositionDataRecord.create()
333 
334         show {
335             Inspectable(slotTableRecord) {
336                 ModalDrawer(
337                     drawerContent = { Text("Something") },
338                     content = {
339                         Column {
340                             Text(text = "Hello World", color = Color.Green)
341                             Button(onClick = {}) { Text(text = "OK") }
342                         }
343                     }
344                 )
345             }
346         }
347         val view = findAndroidComposeView()
348         view.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
349         dumpSlotTableSet(slotTableRecord)
350         val builder = LayoutInspectorTree()
351         val nodes = builder.convert(view)
352         dumpNodes(nodes, view, builder)
353 
354         validate(nodes, builder) {
355             node("ModalDrawer", isRenderNode = true, children = listOf("Column", "Text"))
356             node("Column", inlined = true, children = listOf("Text", "Button"))
357             node("Text", isRenderNode = false)
358             node("Button", isRenderNode = true, children = listOf("Text"))
359             node("Text", isRenderNode = false)
360             node("Text", isRenderNode = false)
361         }
362         assertThat(nodes.size).isEqualTo(1)
363     }
364 
365     @Test
366     fun testStitchTreeFromModelDrawerLayoutWithSystemNodes() {
367         val slotTableRecord = CompositionDataRecord.create()
368 
369         show {
370             Inspectable(slotTableRecord) {
371                 ModalDrawer(
372                     drawerContent = { Text("Something") },
373                     content = {
374                         Column {
375                             Text(text = "Hello World", color = Color.Green)
376                             Button(onClick = {}) { Text(text = "OK") }
377                         }
378                     }
379                 )
380             }
381         }
382         val view = findAndroidComposeView()
383         view.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
384         dumpSlotTableSet(slotTableRecord)
385         val builder = LayoutInspectorTree()
386         builder.hideSystemNodes = false
387         val nodes = builder.convert(view)
388         dumpNodes(nodes, view, builder)
389 
390         if (DEBUG) {
391             validate(nodes, builder) {
392                 node("ModalDrawer", children = listOf("WithConstraints"))
393                 node("WithConstraints", children = listOf("SubcomposeLayout"))
394                 node("SubcomposeLayout", children = listOf("Box"))
395                 node("Box", children = listOf("Box", "Canvas", "Surface"))
396                 node("Box", children = listOf("Column"))
397                 node("Column", children = listOf("Text", "Button"))
398                 node("Text", children = listOf("Text"))
399                 node("Text", children = listOf("CoreText"))
400                 node("CoreText", children = listOf())
401                 node("Button", children = listOf("Surface"))
402                 node("Surface", children = listOf("ProvideTextStyle"))
403                 node("ProvideTextStyle", children = listOf("Row"))
404                 node("Row", children = listOf("Text"))
405                 node("Text", children = listOf("Text"))
406                 node("Text", children = listOf("CoreText"))
407                 node("CoreText", children = listOf())
408                 node("Canvas", children = listOf("Spacer"))
409                 node("Spacer", children = listOf())
410                 node("Surface", children = listOf("Column"))
411                 node("Column", children = listOf("Text"))
412                 node("Text", children = listOf("Text"))
413                 node("Text", children = listOf("CoreText"))
414                 node("CoreText", children = listOf())
415             }
416         }
417         assertThat(nodes.size).isEqualTo(1)
418     }
419 
420     @Test
421     fun testSpacer() {
422         val slotTableRecord = CompositionDataRecord.create()
423 
424         show {
425             Inspectable(slotTableRecord) {
426                 Column {
427                     Text(text = "Hello World", color = Color.Green)
428                     Spacer(Modifier.height(16.dp))
429                     Image(Icons.Filled.Call, null)
430                 }
431             }
432         }
433 
434         val view = findAndroidComposeView()
435         view.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
436         val builder = LayoutInspectorTree()
437         val node = builder.convert(view).flatMap { flatten(it) }.firstOrNull { it.name == "Spacer" }
438 
439         // Spacer should show up in the Compose tree:
440         assertThat(node).isNotNull()
441     }
442 
443     @Test // regression test b/174855322
444     fun testBasicText() {
445         val slotTableRecord = CompositionDataRecord.create()
446 
447         show {
448             Inspectable(slotTableRecord) {
449                 Column {
450                     BasicText(
451                         text = "Some text",
452                         style = TextStyle(textDecoration = TextDecoration.Underline)
453                     )
454                 }
455             }
456         }
457 
458         val view = findAndroidComposeView()
459         view.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
460         val builder = LayoutInspectorTree()
461         builder.includeAllParameters = false
462         val node =
463             builder.convert(view).flatMap { flatten(it) }.firstOrNull { it.name == "BasicText" }
464 
465         assertThat(node).isNotNull()
466         assertThat(node?.parameters).isEmpty()
467 
468         // Get parameters for the Spacer after getting the tree without parameters:
469         val paramsNode = builder.findParameters(view, node!!.anchorId)!!
470         val params =
471             builder.convertParameters(
472                 ROOT_ID,
473                 paramsNode,
474                 ParameterKind.Normal,
475                 MAX_RECURSIONS,
476                 MAX_ITERABLE_SIZE
477             )
478         assertThat(params).isNotEmpty()
479         val text = params.find { it.name == "$0" }
480         assertThat(text?.value).isEqualTo("Some text")
481     }
482 
483     @Test
484     fun testTextId() {
485         val slotTableRecord = CompositionDataRecord.create()
486 
487         show { Inspectable(slotTableRecord) { Text(text = "Hello World") } }
488 
489         val view = findAndroidComposeView()
490         view.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
491         val builder = LayoutInspectorTree()
492         val node = builder.convert(view).flatMap { flatten(it) }.firstOrNull { it.name == "Text" }
493 
494         // LayoutNode id should be captured by the Text node:
495         assertThat(node?.id).isGreaterThan(0)
496     }
497 
498     @Test
499     fun testSemantics() {
500         val slotTableRecord = CompositionDataRecord.create()
501 
502         show {
503             Inspectable(slotTableRecord) {
504                 Column {
505                     Text(text = "Studio")
506                     Row(modifier = Modifier.semantics(true) {}) {
507                         Text(text = "Hello")
508                         Text(text = "World")
509                     }
510                     Row(modifier = Modifier.clearAndSetSemantics { text = AnnotatedString("to") }) {
511                         Text(text = "Hello")
512                         Text(text = "World")
513                     }
514                 }
515             }
516         }
517 
518         val androidComposeView = findAndroidComposeView()
519         androidComposeView.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
520         val builder = LayoutInspectorTree()
521         val nodes = builder.convert(androidComposeView)
522         validate(nodes, builder, checkSemantics = true) {
523             node("Column", children = listOf("Text", "Row", "Row"), inlined = true)
524             node(
525                 name = "Text",
526                 isRenderNode = false,
527                 mergedSemantics = "[Studio]",
528                 unmergedSemantics = "[Studio]",
529             )
530             node(
531                 name = "Row",
532                 children = listOf("Text", "Text"),
533                 mergedSemantics = "[Hello, World]",
534                 inlined = true,
535             )
536             node("Text", isRenderNode = false, unmergedSemantics = "[Hello]")
537             node("Text", isRenderNode = false, unmergedSemantics = "[World]")
538             node(
539                 name = "Row",
540                 children = listOf("Text", "Text"),
541                 mergedSemantics = "[to]",
542                 unmergedSemantics = "[to]",
543                 inlined = true,
544             )
545             node("Text", isRenderNode = false, unmergedSemantics = "[Hello]")
546             node("Text", isRenderNode = false, unmergedSemantics = "[World]")
547         }
548     }
549 
550     @Test
551     fun testDialog() {
552         val slotTableRecord = CompositionDataRecord.create()
553 
554         show {
555             Inspectable(slotTableRecord) {
556                 Column(modifier = Modifier.fillMaxSize()) {
557                     Text("Hello World!")
558                     AlertDialog(
559                         onDismissRequest = {},
560                         confirmButton = { Button({}) { Text("This is the Confirm Button") } }
561                     )
562                 }
563             }
564         }
565         val composeViews = findAllAndroidComposeViews()
566         val appView = composeViews[0]
567         val dialogView = composeViews[1]
568         assertThat(composeViews).hasSize(2)
569         appView.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
570         dialogView.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
571 
572         val builder = LayoutInspectorTree()
573 
574         val allNodes = builder.convert(listOf(appView, dialogView))
575         val appNodes = allNodes[appView.uniqueDrawingId] ?: emptyList()
576         dumpSlotTableSet(slotTableRecord)
577         dumpNodes(appNodes, appView, builder)
578 
579         // Verify that the main app does not contain the Popup
580         validate(appNodes, builder) {
581             node(
582                 name = "Column",
583                 fileName = "LayoutInspectorTreeTest.kt",
584                 children = listOf("Text"),
585                 inlined = true,
586                 isRenderNode = true,
587             )
588             node(
589                 name = "Text",
590                 isRenderNode = false,
591                 fileName = "LayoutInspectorTreeTest.kt",
592             )
593         }
594 
595         val dialogNodes = allNodes[dialogView.uniqueDrawingId] ?: emptyList()
596         dumpNodes(dialogNodes, dialogView, builder)
597 
598         // Verify that the AlertDialog is captured with content
599         validate(dialogNodes, builder) {
600             node(
601                 name = "AlertDialog",
602                 fileName = "LayoutInspectorTreeTest.kt",
603                 children = listOf("Button")
604             )
605             node(
606                 name = "Button",
607                 fileName = "LayoutInspectorTreeTest.kt",
608                 isRenderNode = true,
609                 children = listOf("Text")
610             )
611             node(
612                 name = "Text",
613                 isRenderNode = false,
614                 fileName = "LayoutInspectorTreeTest.kt",
615             )
616         }
617     }
618 
619     @Test
620     fun testPopup() {
621         val slotTableRecord = CompositionDataRecord.create()
622 
623         show {
624             Inspectable(slotTableRecord) {
625                 Column(modifier = Modifier.fillMaxSize()) {
626                     Text("Compose Text")
627                     Popup(alignment = Alignment.Center) { Text("This is a popup") }
628                 }
629             }
630         }
631         val composeViews = findAllAndroidComposeViews()
632         val appView = composeViews[0]
633         val popupView = composeViews[1]
634         appView.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
635         popupView.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
636         val builder = LayoutInspectorTree()
637 
638         val allNodes = builder.convert(listOf(appView, popupView))
639         val appNodes = allNodes[appView.uniqueDrawingId] ?: emptyList()
640         dumpNodes(appNodes, appView, builder)
641 
642         // Verify that the main app does not contain the Popup
643         validate(appNodes, builder) {
644             node(
645                 name = "Column",
646                 isRenderNode = true,
647                 fileName = "LayoutInspectorTreeTest.kt",
648                 children = listOf("Text"),
649                 inlined = true,
650             )
651             node(
652                 name = "Text",
653                 isRenderNode = false,
654                 fileName = "LayoutInspectorTreeTest.kt",
655             )
656         }
657 
658         val popupNodes = allNodes[popupView.uniqueDrawingId] ?: emptyList()
659         dumpNodes(popupNodes, popupView, builder)
660 
661         // Verify that the Popup is captured with content
662         validate(popupNodes, builder) {
663             node(name = "Popup", fileName = "LayoutInspectorTreeTest.kt", children = listOf("Text"))
664             node(
665                 name = "Text",
666                 isRenderNode = false,
667                 fileName = "LayoutInspectorTreeTest.kt",
668             )
669         }
670     }
671 
672     @Test
673     fun testAndroidView() {
674         val slotTableRecord = CompositionDataRecord.create()
675 
676         show {
677             Inspectable(slotTableRecord) {
678                 Column {
679                     Text("Compose Text")
680                     AndroidView({ context -> TextView(context).apply { text = "AndroidView" } })
681                 }
682             }
683         }
684         val composeView = findAndroidComposeView() as ViewGroup
685         composeView.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
686         val builder = LayoutInspectorTree()
687         builder.hideSystemNodes = false
688         val nodes = builder.convert(composeView)
689         dumpNodes(nodes, composeView, builder)
690         val androidView = nodes.flatMap { flatten(it) }.first { it.name == "AndroidView" }
691         assertThat(androidView.viewId).isEqualTo(0)
692 
693         validate(listOf(androidView), builder) {
694             node(
695                 name = "AndroidView",
696                 fileName = "LayoutInspectorTreeTest.kt",
697                 children = listOf("AndroidView")
698             )
699             node(
700                 name = "AndroidView",
701                 fileName = "AndroidView.android.kt",
702                 children = listOf("ComposeNode")
703             )
704             node(
705                 name = "ComposeNode",
706                 fileName = "AndroidView.android.kt",
707                 hasViewIdUnder = composeView,
708                 isRenderNode = true,
709                 inlined = true,
710             )
711         }
712     }
713 
714     @Test
715     fun testAndroidViewWithOnResetOverload() {
716         val slotTableRecord = CompositionDataRecord.create()
717 
718         show {
719             Inspectable(slotTableRecord) {
720                 Column {
721                     Text("Compose Text")
722                     AndroidView(
723                         factory = { context -> TextView(context).apply { text = "AndroidView" } },
724                         onReset = {
725                             // Do nothing, just use the overload.
726                         }
727                     )
728                 }
729             }
730         }
731         val composeView = findAndroidComposeView() as ViewGroup
732         composeView.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
733         val builder = LayoutInspectorTree()
734         builder.hideSystemNodes = false
735         val nodes = builder.convert(composeView)
736         dumpNodes(nodes, composeView, builder)
737         val androidView = nodes.flatMap { flatten(it) }.first { it.name == "AndroidView" }
738         assertThat(androidView.viewId).isEqualTo(0)
739 
740         validate(listOf(androidView), builder) {
741             node(
742                 name = "AndroidView",
743                 fileName = "LayoutInspectorTreeTest.kt",
744                 children = listOf("ReusableComposeNode")
745             )
746             node(
747                 name = "ReusableComposeNode",
748                 fileName = "AndroidView.android.kt",
749                 hasViewIdUnder = composeView,
750                 isRenderNode = true,
751                 inlined = true,
752             )
753         }
754     }
755 
756     @Test
757     fun testDoubleAndroidView() {
758         val slotTableRecord = CompositionDataRecord.create()
759 
760         show {
761             Inspectable(slotTableRecord) {
762                 Column {
763                     Text("Compose Text1")
764                     AndroidView({ context -> TextView(context).apply { text = "first" } })
765                     Text("Compose Text2")
766                     AndroidView({ context -> TextView(context).apply { text = "second" } })
767                 }
768             }
769         }
770         val composeView = findAndroidComposeView() as ViewGroup
771         composeView.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
772         val builder = LayoutInspectorTree()
773         builder.hideSystemNodes = false
774         val nodes = builder.convert(composeView)
775         dumpSlotTableSet(slotTableRecord)
776         dumpNodes(nodes, composeView, builder)
777         val textViews = findAllViews("TextView")
778         val firstTextView = textViews.filterIsInstance<TextView>().first { it.text == "first" }
779         val secondTextView = textViews.filterIsInstance<TextView>().first { it.text == "second" }
780         val composeNodes = nodes.flatMap { it.flatten() }.filter { it.name == "ComposeNode" }
781         assertThat(composeNodes).hasSize(2)
782         assertThat(composeNodes[0].viewId).isEqualTo(viewParent(secondTextView)?.uniqueDrawingId)
783         assertThat(composeNodes[1].viewId).isEqualTo(viewParent(firstTextView)?.uniqueDrawingId)
784     }
785 
786     // WARNING: The formatting of the lines below here affect test results.
787     val titleLine = Throwable().stackTrace[0].lineNumber + 3
788 
789     @Composable
790     private fun Title() {
791         val maxOffset = with(LocalDensity.current) { 80.dp.toPx() }
792         val minOffset = with(LocalDensity.current) { 80.dp.toPx() }
793         val offset = maxOffset.coerceAtLeast(minOffset)
794         Column(
795             verticalArrangement = Arrangement.Bottom,
796             modifier =
797                 Modifier.heightIn(min = 128.dp)
798                     .graphicsLayer { translationY = offset }
799                     .background(color = MaterialTheme.colors.background)
800         ) {
801             Spacer(Modifier.height(16.dp))
802             Text(
803                 text = "Snack",
804                 style = MaterialTheme.typography.h4,
805                 color = MaterialTheme.colors.secondary,
806                 modifier = Modifier.padding(horizontal = 24.dp)
807             )
808             Text(
809                 text = "Tagline",
810                 style = MaterialTheme.typography.subtitle2,
811                 fontSize = 20.sp,
812                 color = MaterialTheme.colors.secondary,
813                 modifier = Modifier.padding(horizontal = 24.dp)
814             )
815             Spacer(Modifier.height(4.dp))
816             Text(
817                 text = "$2.95",
818                 style = MaterialTheme.typography.h6,
819                 color = MaterialTheme.colors.primary,
820                 modifier = Modifier.padding(horizontal = 24.dp)
821             )
822             Spacer(Modifier.height(8.dp))
823         }
824     }
825 
826     // WARNING: End formatted section
827 
828     @Test
829     fun testLineNumbers() {
830         // WARNING: The formatting of the lines below here affect test results.
831         val testLine = Throwable().stackTrace[0].lineNumber
832         val slotTableRecord = CompositionDataRecord.create()
833 
834         show { Inspectable(slotTableRecord) { Column { Title() } } }
835         // WARNING: End formatted section
836 
837         val androidComposeView = findAndroidComposeView()
838         androidComposeView.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
839         val builder = LayoutInspectorTree()
840         val nodes = builder.convert(androidComposeView)
841         dumpNodes(nodes, androidComposeView, builder)
842 
843         validate(nodes, builder, checkLineNumbers = true, checkRenderNodes = false) {
844             node("Column", lineNumber = testLine + 3, children = listOf("Title"), inlined = true)
845             node("Title", lineNumber = testLine + 3, children = listOf("Column"))
846             node(
847                 name = "Column",
848                 lineNumber = titleLine + 4,
849                 children = listOf("Spacer", "Text", "Text", "Spacer", "Text", "Spacer"),
850                 inlined = true,
851             )
852             node("Spacer", lineNumber = titleLine + 11)
853             node("Text", lineNumber = titleLine + 12)
854             node("Text", lineNumber = titleLine + 18)
855             node("Spacer", lineNumber = titleLine + 25)
856             node("Text", lineNumber = titleLine + 26)
857             node("Spacer", lineNumber = titleLine + 32)
858         }
859     }
860 
861     @Composable
862     @Suppress("UNUSED_PARAMETER")
863     fun First(p1: Int) {
864         Text("First")
865     }
866 
867     @Composable
868     @Suppress("UNUSED_PARAMETER")
869     fun Second(p2: Int) {
870         Text("Second")
871     }
872 
873     @Test
874     fun testCrossfade() {
875         val slotTableRecord = CompositionDataRecord.create()
876 
877         show {
878             Inspectable(slotTableRecord) {
879                 Column {
880                     var showFirst by remember { mutableStateOf(true) }
881                     Button(onClick = { showFirst = !showFirst }) { Text("Button") }
882                     Crossfade(showFirst) {
883                         when (it) {
884                             true -> First(p1 = 1)
885                             false -> Second(p2 = 2)
886                         }
887                     }
888                 }
889             }
890         }
891         val androidComposeView = findAndroidComposeView()
892         androidComposeView.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
893         val builder = LayoutInspectorTree()
894         builder.includeAllParameters = true
895         val tree1 = builder.convert(androidComposeView)
896         val first = tree1.flatMap { flatten(it) }.single { it.name == "First" }
897         val hash = packageNameHash(this.javaClass.name.substringBeforeLast('.'))
898         assertThat(first.fileName).isEqualTo("LayoutInspectorTreeTest.kt")
899         assertThat(first.packageHash).isEqualTo(hash)
900         assertThat(first.parameters.map { it.name }).contains("p1")
901 
902         val cross1 = tree1.flatMap { flatten(it) }.single { it.name == "Crossfade" }
903         val button1 = tree1.flatMap { flatten(it) }.single { it.name == "Button" }
904         val column1 = tree1.flatMap { flatten(it) }.single { it.name == "Column" }
905 
906         assertThat(cross1.id).isGreaterThan(RESERVED_FOR_GENERATED_IDS)
907         assertThat(button1.id).isGreaterThan(RESERVED_FOR_GENERATED_IDS)
908         assertThat(column1.id).isLessThan(RESERVED_FOR_GENERATED_IDS)
909 
910         composeTestRule.onNodeWithText("Button").performClick()
911         composeTestRule.runOnIdle {
912             val tree2 = builder.convert(androidComposeView)
913             val second = tree2.flatMap { flatten(it) }.first { it.name == "Second" }
914             assertThat(second.fileName).isEqualTo("LayoutInspectorTreeTest.kt")
915             assertThat(second.packageHash).isEqualTo(hash)
916             assertThat(second.parameters.map { it.name }).contains("p2")
917 
918             val cross2 = tree2.flatMap { flatten(it) }.first { it.name == "Crossfade" }
919             val button2 = tree2.flatMap { flatten(it) }.single { it.name == "Button" }
920             val column2 = tree2.flatMap { flatten(it) }.single { it.name == "Column" }
921             assertThat(cross2.id).isNotEqualTo(cross1.id)
922             assertThat(button2.id).isEqualTo(button1.id)
923             assertThat(column2.id).isEqualTo(column1.id)
924         }
925     }
926 
927     @Test
928     fun testInlineParameterTypes() {
929         val slotTableRecord = CompositionDataRecord.create()
930 
931         show { Inspectable(slotTableRecord) { InlineParameters(20.5.dp, 30.sp) } }
932         val androidComposeView = findAndroidComposeView()
933         androidComposeView.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
934         val builder = LayoutInspectorTree()
935         builder.hideSystemNodes = false
936         builder.includeAllParameters = true
937         val inlineParameters =
938             builder
939                 .convert(androidComposeView)
940                 .flatMap { flatten(it) }
941                 .first { it.name == "InlineParameters" }
942         assertThat(inlineParameters.parameters[0].name).isEqualTo("size")
943         assertThat(inlineParameters.parameters[0].value?.javaClass).isEqualTo(Dp::class.java)
944         assertThat(inlineParameters.parameters[1].name).isEqualTo("fontSize")
945         assertThat(inlineParameters.parameters[1].value?.javaClass).isEqualTo(TextUnit::class.java)
946         assertThat(inlineParameters.parameters).hasSize(2)
947     }
948 
949     @Test
950     fun testRemember() {
951         val slotTableRecord = CompositionDataRecord.create()
952 
953         // Regression test for: b/235526153
954         // The method: SubCompositionRoots.remember had code like this:
955         //   group.data.filterIsInstance<Ref<ViewRootForInspector>>().singleOrNull()?.value
956         // which would cause a ClassCastException if the data contained a Ref to something
957         // else than a ViewRootForInspector instance. This would crash the app.
958         show {
959             Inspectable(slotTableRecord) {
960                 rememberCompositionContext()
961                 val stringReference = remember { Ref<String>() }
962                 stringReference.value = "Hello"
963             }
964         }
965         val androidComposeView = findAndroidComposeView()
966         androidComposeView.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
967         val builder = LayoutInspectorTree()
968         builder.hideSystemNodes = false
969         builder.includeAllParameters = false
970     }
971 
972     @Test // regression test for b/311436726
973     fun testLazyColumn() {
974         val slotTableRecord = CompositionDataRecord.create()
975 
976         show {
977             Inspectable(slotTableRecord) {
978                 LazyColumn(modifier = Modifier.testTag("LazyColumn")) {
979                     items(100) { index -> Text(text = "Item: $index") }
980                 }
981             }
982         }
983 
984         val androidComposeView = findAndroidComposeView()
985         androidComposeView.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
986         val builder = LayoutInspectorTree()
987         builder.hideSystemNodes = false
988         builder.includeAllParameters = true
989         ThreadUtils.runOnMainThread { builder.convert(androidComposeView) }
990         for (index in 20..40) {
991             composeTestRule.onNodeWithTag("LazyColumn").performScrollToIndex(index)
992         }
993         ThreadUtils.runOnMainThread { builder.convert(androidComposeView) }
994     }
995 
996     @Test
997     fun testScaffold() {
998         val slotTableRecord = CompositionDataRecord.create()
999 
1000         show {
1001             Inspectable(slotTableRecord) {
1002                 Scaffold { Column { LinearProgressIndicator(progress = 0.3F) } }
1003             }
1004         }
1005         val androidComposeView = findAndroidComposeView()
1006         androidComposeView.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
1007         val builder = LayoutInspectorTree()
1008         builder.hideSystemNodes = false
1009         builder.includeAllParameters = true
1010         val linearProgressIndicator =
1011             builder
1012                 .convert(androidComposeView)
1013                 .flatMap { flatten(it) }
1014                 .firstOrNull { it.name == "LinearProgressIndicator" }
1015         assertThat(linearProgressIndicator).isNotNull()
1016     }
1017 
1018     @Suppress("SameParameterValue")
1019     private fun validate(
1020         result: List<InspectorNode>,
1021         builder: LayoutInspectorTree,
1022         checkParameters: Boolean = false,
1023         checkSemantics: Boolean = false,
1024         checkLineNumbers: Boolean = false,
1025         checkRenderNodes: Boolean = true,
1026         density: Density = this.density,
1027         block: TreeValidationReceiver.() -> Unit = {}
1028     ) {
1029         if (DEBUG) {
1030             return
1031         }
1032         val nodes = result.flatMap { flatten(it) }.listIterator()
1033         val tree =
1034             TreeValidationReceiver(
1035                 nodes,
1036                 density,
1037                 checkParameters,
1038                 checkSemantics,
1039                 checkLineNumbers,
1040                 checkRenderNodes,
1041                 builder
1042             )
1043         tree.block()
1044     }
1045 
1046     private class TreeValidationReceiver(
1047         val nodeIterator: Iterator<InspectorNode>,
1048         val density: Density,
1049         val checkParameters: Boolean,
1050         val checkSemantics: Boolean,
1051         val checkLineNumbers: Boolean,
1052         val checkRenderNodes: Boolean,
1053         val builder: LayoutInspectorTree
1054     ) {
1055         fun node(
1056             name: String,
1057             fileName: String? = null,
1058             lineNumber: Int = -1,
1059             isRenderNode: Boolean = false,
1060             inlined: Boolean = false,
1061             hasViewIdUnder: View? = null,
1062             hasTransformations: Boolean = false,
1063             mergedSemantics: String = "",
1064             unmergedSemantics: String = "",
1065             left: Dp = Dp.Unspecified,
1066             top: Dp = Dp.Unspecified,
1067             width: Dp = Dp.Unspecified,
1068             height: Dp = Dp.Unspecified,
1069             children: List<String> = listOf(),
1070             block: ParameterValidationReceiver.() -> Unit = {}
1071         ) {
1072             assertWithMessage("No such node found: $name").that(nodeIterator.hasNext()).isTrue()
1073             val node = nodeIterator.next()
1074             assertThat(node.name).isEqualTo(name)
1075             assertThat(node.anchorId).isNotEqualTo(UNDEFINED_ID)
1076             val message = "Node: $name"
1077             assertWithMessage(message)
1078                 .that(node.children.map { it.name })
1079                 .containsExactlyElementsIn(children)
1080                 .inOrder()
1081             fileName?.let { assertWithMessage(message).that(node.fileName).isEqualTo(fileName) }
1082             if (lineNumber != -1) {
1083                 assertWithMessage(message).that(node.lineNumber).isEqualTo(lineNumber)
1084             }
1085             assertWithMessage(message).that(node.inlined).isEqualTo(inlined)
1086             if (checkRenderNodes) {
1087                 if (isRenderNode) {
1088                     assertWithMessage(message).that(node.id).isGreaterThan(0L)
1089                 } else {
1090                     assertWithMessage(message).that(node.id).isLessThan(0L)
1091                 }
1092             }
1093             if (hasViewIdUnder != null) {
1094                 assertWithMessage(message).that(node.viewId).isGreaterThan(0L)
1095                 assertWithMessage(message).that(hasViewIdUnder.hasChild(node.viewId)).isTrue()
1096             } else {
1097                 assertWithMessage(message).that(node.viewId).isEqualTo(0L)
1098             }
1099             if (hasTransformations) {
1100                 assertWithMessage(message).that(node.bounds).isNotNull()
1101             } else {
1102                 assertWithMessage(message).that(node.bounds).isNull()
1103             }
1104             if (left != Dp.Unspecified) {
1105                 with(density) {
1106                     assertWithMessage(message)
1107                         .that(node.left.toDp().value)
1108                         .isWithin(2.0f)
1109                         .of(left.value)
1110                     assertWithMessage(message)
1111                         .that(node.top.toDp().value)
1112                         .isWithin(2.0f)
1113                         .of(top.value)
1114                     assertWithMessage(message)
1115                         .that(node.width.toDp().value)
1116                         .isWithin(2.0f)
1117                         .of(width.value)
1118                     assertWithMessage(message)
1119                         .that(node.height.toDp().value)
1120                         .isWithin(2.0f)
1121                         .of(height.value)
1122                 }
1123             }
1124 
1125             if (checkSemantics) {
1126                 val merged = node.mergedSemantics.singleOrNull { it.name == "Text" }?.value
1127                 assertWithMessage(message).that(merged?.toString() ?: "").isEqualTo(mergedSemantics)
1128                 val unmerged = node.unmergedSemantics.singleOrNull { it.name == "Text" }?.value
1129                 assertWithMessage(message)
1130                     .that(unmerged?.toString() ?: "")
1131                     .isEqualTo(unmergedSemantics)
1132             }
1133 
1134             if (checkLineNumbers) {
1135                 assertThat(node.lineNumber).isEqualTo(lineNumber)
1136             }
1137 
1138             if (checkParameters) {
1139                 val params =
1140                     builder.convertParameters(
1141                         ROOT_ID,
1142                         node,
1143                         ParameterKind.Normal,
1144                         MAX_RECURSIONS,
1145                         MAX_ITERABLE_SIZE
1146                     )
1147                 val receiver = ParameterValidationReceiver(params.listIterator())
1148                 receiver.block()
1149                 receiver.checkFinished(name)
1150             }
1151         }
1152 
1153         private fun View.hasChild(id: Long): Boolean {
1154             if (uniqueDrawingId == id) {
1155                 return true
1156             }
1157             if (this !is ViewGroup) {
1158                 return false
1159             }
1160             for (index in 0..childCount) {
1161                 if (getChildAt(index).hasChild(id)) {
1162                     return true
1163                 }
1164             }
1165             return false
1166         }
1167     }
1168 
1169     private fun flatten(node: InspectorNode): List<InspectorNode> =
1170         listOf(node).plus(node.children.flatMap { flatten(it) })
1171 
1172     private fun viewParent(view: View): View? = view.parent as? View
1173 
1174     private fun show(composable: @Composable () -> Unit) = composeTestRule.setContent(composable)
1175 
1176     // region DEBUG print methods
1177     private fun dumpNodes(nodes: List<InspectorNode>, view: View, builder: LayoutInspectorTree) {
1178         @Suppress("ConstantConditionIf")
1179         if (!DEBUG) {
1180             return
1181         }
1182         println()
1183         println("=================== Nodes ==========================")
1184         nodes.forEach { dumpNode(it, indent = 0) }
1185         println()
1186         println("=================== validate statements ==========================")
1187         nodes.forEach { generateValidate(it, view, builder) }
1188     }
1189 
1190     private fun dumpNode(node: InspectorNode, indent: Int) {
1191         println(
1192             "\"${"  ".repeat(indent * 2)}\", \"${node.name}\", \"${node.fileName}\", " +
1193                 "${node.lineNumber}, ${node.left}, ${node.top}, " +
1194                 "${node.width}, ${node.height}"
1195         )
1196         node.children.forEach { dumpNode(it, indent + 1) }
1197     }
1198 
1199     private fun generateValidate(
1200         node: InspectorNode,
1201         view: View,
1202         builder: LayoutInspectorTree,
1203         generateParameters: Boolean = false
1204     ) {
1205         with(density) {
1206             val left = round(node.left.toDp())
1207             val top = round(node.top.toDp())
1208             val width = if (node.width == view.width) "viewWidth" else round(node.width.toDp())
1209             val height = if (node.height == view.height) "viewHeight" else round(node.height.toDp())
1210 
1211             print(
1212                 """
1213                   validate(
1214                       name = "${node.name}",
1215                       fileName = "${node.fileName}",
1216                       left = $left, top = $top, width = $width, height = $height
1217                 """
1218                     .trimIndent()
1219             )
1220         }
1221         if (node.id > 0L) {
1222             println(",")
1223             print("    isRenderNode = true")
1224         }
1225         if (node.children.isNotEmpty()) {
1226             println(",")
1227             val children = node.children.joinToString { "\"${it.name}\"" }
1228             print("    children = listOf($children)")
1229         }
1230         println()
1231         print(")")
1232         if (generateParameters && node.parameters.isNotEmpty()) {
1233             generateParameters(
1234                 builder.convertParameters(
1235                     ROOT_ID,
1236                     node,
1237                     ParameterKind.Normal,
1238                     MAX_RECURSIONS,
1239                     MAX_ITERABLE_SIZE
1240                 ),
1241                 0
1242             )
1243         }
1244         println()
1245         node.children.forEach { generateValidate(it, view, builder) }
1246     }
1247 
1248     private fun generateParameters(parameters: List<NodeParameter>, indent: Int) {
1249         val indentation = " ".repeat(indent * 2)
1250         println(" {")
1251         for (param in parameters) {
1252             val name = param.name
1253             val type = param.type
1254             val value = toDisplayValue(type, param.value)
1255             print("$indentation  parameter(name = \"$name\", type = $type, value = $value)")
1256             if (param.elements.isNotEmpty()) {
1257                 generateParameters(param.elements, indent + 1)
1258             }
1259             println()
1260         }
1261         print("$indentation}")
1262     }
1263 
1264     private fun toDisplayValue(type: ParameterType, value: Any?): String =
1265         when (type) {
1266             ParameterType.Boolean -> value.toString()
1267             ParameterType.Color ->
1268                 "0x${Integer.toHexString(value as Int)}${if (value < 0) ".toInt()" else ""}"
1269             ParameterType.DimensionSp,
1270             ParameterType.DimensionDp -> "${value}f"
1271             ParameterType.Int32 -> value.toString()
1272             ParameterType.String -> "\"$value\""
1273             else -> value?.toString() ?: "null"
1274         }
1275 
1276     private fun dumpSlotTableSet(slotTableRecord: CompositionDataRecord) {
1277         @Suppress("ConstantConditionIf")
1278         if (!DEBUG) {
1279             return
1280         }
1281         println()
1282         println("=================== Groups ==========================")
1283         slotTableRecord.store.forEach { dumpGroup(it.asTree(), indent = 0) }
1284     }
1285 
1286     private fun dumpGroup(group: Group, indent: Int) {
1287         val location = group.location
1288         val position = group.position?.let { "\"$it\"" } ?: "null"
1289         val box = group.box
1290         val id =
1291             group.modifierInfo
1292                 .mapNotNull { (it.extra as? GraphicLayerInfo)?.layerId }
1293                 .singleOrNull() ?: 0
1294         println(
1295             "\"${"  ".repeat(indent)}\", ${group.javaClass.simpleName}, \"${group.name}\", " +
1296                 "file: ${location?.sourceFile}  hash: ${location?.packageHash}, " +
1297                 "params: ${group.parameters.size}, children: ${group.children.size}, " +
1298                 "$id, $position, " +
1299                 "${box.left}, ${box.right}, ${box.right - box.left}, ${box.bottom - box.top}"
1300         )
1301         for (parameter in group.parameters) {
1302             println("\"${"  ".repeat(indent + 4)}\"- ${parameter.name}")
1303         }
1304         group.children.forEach { dumpGroup(it, indent + 1) }
1305     }
1306 
1307     private fun round(dp: Dp): Dp = Dp((dp.value * 10.0f).roundToInt() / 10.0f)
1308 
1309     // endregion
1310 }
1311 
1312 /** Storage for the preview generated [CompositionData]s. */
1313 internal interface CompositionDataRecord {
1314     val store: Set<CompositionData>
1315 
1316     companion object {
createnull1317         fun create(): CompositionDataRecord = CompositionDataRecordImpl()
1318     }
1319 }
1320 
1321 private class CompositionDataRecordImpl : CompositionDataRecord {
1322     @OptIn(InternalComposeApi::class)
1323     override val store: MutableSet<CompositionData> = Collections.newSetFromMap(WeakHashMap())
1324 }
1325 
1326 /**
1327  * A wrapper for compositions in inspection mode. The composition inside the Inspectable component
1328  * is in inspection mode.
1329  *
1330  * @param compositionDataRecord [CompositionDataRecord] to record the SlotTable used in the
1331  *   composition of [content]
1332  */
1333 @Composable
1334 @OptIn(InternalComposeApi::class)
1335 internal fun Inspectable(
1336     compositionDataRecord: CompositionDataRecord,
1337     content: @Composable () -> Unit
1338 ) {
1339     currentComposer.collectParameterInformation()
1340     val store = (compositionDataRecord as CompositionDataRecordImpl).store
1341     store.add(currentComposer.compositionData)
1342     CompositionLocalProvider(
1343         LocalInspectionMode provides true,
1344         LocalInspectionTables provides store,
1345         content = content
1346     )
1347 }
1348 
1349 @Composable
InlineParametersnull1350 fun InlineParameters(size: Dp, fontSize: TextUnit) {
1351     Text("$size $fontSize")
1352 }
1353 
LayoutInspectorTreenull1354 fun LayoutInspectorTree.convert(view: View): List<InspectorNode> =
1355     convert(listOf(view))[view.uniqueDrawingId] ?: emptyList()
1356