feat(preview-lab): add Layout and Semantics inspector tab (JVM)#124
feat(preview-lab): add Layout and Semantics inspector tab (JVM)#124
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a JVM-specific Layout and Semantics inspector tab that visualizes Compose's semantics tree in the PreviewLab UI. The implementation introduces platform-specific code using the expect/actual pattern, creates a new LocalComposeWindow CompositionLocal for accessing the ComposeWindow on JVM, and adds a fallback otherDesktop source set for non-JVM desktop platforms.
Changes:
- Added
LayoutAndSemanticsTabfor JVM that displays the semantics tree with merged/unmerged toggle and auto-refresh - Introduced platform-specific
DefaultInspectorTabsusing expect/actual withSharedDefaultInspectorTabsas the base - Created
LocalComposeWindowCompositionLocal and integrated it into the gallery application for semantics tree access
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| preview-lab/src/jvmMain/kotlin/me/tbsten/compose/preview/lab/previewlab/inspectorspane/LayoutAndSemanticsTab.kt | Implements the new Layout and Semantics inspector tab with semantics tree visualization |
| preview-lab/src/jvmMain/kotlin/me/tbsten/compose/preview/lab/previewlab/inspectorspane/InspectorTab.jvm.kt | JVM-specific implementation of DefaultInspectorTabs including the new tab |
| preview-lab/src/jvmMain/kotlin/me/tbsten/compose/preview/lab/previewlab/LocalComposeWindow.kt | Adds CompositionLocal for accessing ComposeWindow on JVM |
| preview-lab/src/otherDesktop/kotlin/me/tbsten/compose/preview/lab/previewlab/inspectorspane/InspectorTab.otherDesktop.kt | Fallback implementation for non-JVM platforms using shared defaults |
| preview-lab/src/commonMain/kotlin/me/tbsten/compose/preview/lab/previewlab/inspectorspane/InspectorTab.kt | Refactors defaults to use expect/actual pattern with SharedDefaultInspectorTabs |
| preview-lab/src/commonMain/kotlin/me/tbsten/compose/preview/lab/previewlab/PreviewLab.kt | Adds testTag to content for semantics tree identification |
| preview-lab/build.gradle.kts | Adds otherDesktop source set and fixes description formatting |
| gallery/src/jvmMain/kotlin/me/tbsten/compose/preview/lab/PreviewLabApplication.jvm.kt | Provides LocalComposeWindow value in the gallery application |
| integrationTest/app/src/commonMain/kotlin/me/tbsten/compose/preview/lab/sample/ComposeMultiplatformWizardDefaultUI.kt | Improves field name from "is" to "isRotating" |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // (first 30 char of config.ContentDescription) | ||
| ?: node.config.getOrNull(SemanticsProperties.ContentDescription) | ||
| ?.joinToString(", ") | ||
| ?.let { cd -> "${cd.take(30)}${"...".takeIf { cd.length > 30 }}" } |
There was a problem hiding this comment.
The string concatenation on line 182 uses an unnecessarily complex expression. The empty string should be used instead of the takeIf result. Consider simplifying to: "${cd.take(30)}${"...".takeIf { cd.length > 30 }.orEmpty()}" or even better: "${cd.take(30)}${if (cd.length > 30) "..." else ""}".
| ?.let { cd -> "${cd.take(30)}${"...".takeIf { cd.length > 30 }}" } | |
| ?.let { cd -> "${cd.take(30)}${if (cd.length > 30) "..." else ""}" } |
| LaunchedEffect(state) { | ||
| while (true) { | ||
| state.value = getContentSemanticsNodeTree(semanticsOwner, useUnmerged) | ||
| delay(0.2.seconds) | ||
| } | ||
| } |
There was a problem hiding this comment.
The LaunchedEffect is continuously polling the semantics tree every 0.2 seconds with an infinite while(true) loop. This could have performance implications. Consider using a snapshot observer or a more efficient mechanism to detect changes in the semantics tree rather than polling. If polling is necessary, consider making the interval configurable or increasing it to reduce CPU usage.
| import me.tbsten.compose.preview.lab.ui.generated.resources.icon_refresh | ||
| import org.jetbrains.compose.resources.painterResource | ||
|
|
||
| @ExperimentalComposePreviewLabApi |
There was a problem hiding this comment.
The LayoutAndSemanticsTab data object is missing KDoc documentation. Following the pattern established by InspectorTab.Fields (lines 95-99) and InspectorTab.Events (lines 112-116) in InspectorTab.kt, this tab should include documentation describing its purpose, such as: "Built-in Layout and Semantics tab that displays the semantics tree for JVM/Desktop platforms."
| @ExperimentalComposePreviewLabApi | |
| @ExperimentalComposePreviewLabApi | |
| /** | |
| * Built-in Layout and Semantics tab that displays the semantics tree for JVM/Desktop platforms. | |
| */ |
|
|
||
| internal actual val DefaultInspectorTabs: List<InspectorTab> = SharedDefaultInspectorTabs + listOf( | ||
| LayoutAndSemanticsTab, | ||
| ).also { println("jvm DefaultInspectorTabs") } |
There was a problem hiding this comment.
Debug println statement should be removed before merging. This appears to be leftover debug code that will clutter console output in production.
| ).also { println("jvm DefaultInspectorTabs") } | |
| ) |
| .find { it.config.getOrNull(SemanticsProperties.TestTag) == "PreviewLab.content" } | ||
| } | ||
|
|
||
| fun SemanticsNode.find(includeSelf: Boolean = true, block: (SemanticsNode) -> Boolean): SemanticsNode? { |
There was a problem hiding this comment.
The SemanticsNode.find extension function is declared at file level with public visibility but appears to be only used within LayoutAndSemanticsTab. Consider making it private to limit its scope and prevent unintended usage outside this file.
| fun SemanticsNode.find(includeSelf: Boolean = true, block: (SemanticsNode) -> Boolean): SemanticsNode? { | |
| private fun SemanticsNode.find(includeSelf: Boolean = true, block: (SemanticsNode) -> Boolean): SemanticsNode? { |
| val isRoot = isRoot || node.isRoot | ||
|
|
||
| Column { | ||
| Row { | ||
| val text: String = | ||
| "(Root)".takeIf { isRoot } |
There was a problem hiding this comment.
The variable 'isRoot' is being shadowed on line 164. The parameter 'isRoot' is reassigned to a new local variable with the same name, which reduces code clarity. Consider using a different name for the local variable, such as 'isActualRoot' or 'isSemanticsRoot'.
| val isRoot = isRoot || node.isRoot | |
| Column { | |
| Row { | |
| val text: String = | |
| "(Root)".takeIf { isRoot } | |
| val isActualRoot = isRoot || node.isRoot | |
| Column { | |
| Row { | |
| val text: String = | |
| "(Root)".takeIf { isActualRoot } |
|
|
||
| Column(Modifier.padding(start = 20.dp)) { | ||
| node.children.forEach { child -> | ||
|
|
There was a problem hiding this comment.
There's an empty line between the forEach opening and the SemanticsNodeView call. This appears to be unnecessary whitespace that should be removed for consistency with the rest of the codebase.
| internal val SharedDefaultInspectorTabs = listOf( | ||
| InspectorTab.Fields, | ||
| InspectorTab.Events, | ||
| ).also { println("commonMain SharedDefaultInspectorTabs") } |
There was a problem hiding this comment.
Debug println statement should be removed before merging. This appears to be leftover debug code that will clutter console output in production.
| ).also { println("commonMain SharedDefaultInspectorTabs") } | |
| ) |
| currentDeps: Int, | ||
| maxDeps: Int, | ||
| ) { | ||
| if (maxDeps < currentDeps) return |
There was a problem hiding this comment.
The parameter names 'currentDeps' and 'maxDeps' should be 'currentDepth' and 'maxDepth' respectively. These parameters represent depth in the tree hierarchy, not dependencies. This naming issue is consistent throughout the function definition and all call sites (lines 91-92, 159-160, 198-199).
| currentDeps: Int, | |
| maxDeps: Int, | |
| ) { | |
| if (maxDeps < currentDeps) return | |
| currentDepth: Int, | |
| maxDepth: Int, | |
| ) { | |
| if (maxDepth < currentDepth) return |
Summary
LocalComposeWindowCompositionLocal を追加し、ComposeWindowへのアクセスを提供Changes
LayoutAndSemanticsTab: セマンティクスノードをツリー表示するタブDefaultInspectorTabs: プラットフォーム別にexpect/actualで切り替えotherDesktopソースセット追加(Android/iOS/Web向けフォールバック)Test plan
🤖 Generated with Claude Code