Skip to content

Comments

feat(preview-lab): add Layout and Semantics inspector tab (JVM)#124

Draft
TBSten wants to merge 1 commit intomainfrom
feature/inspect-layout-and-semantics-tab
Draft

feat(preview-lab): add Layout and Semantics inspector tab (JVM)#124
TBSten wants to merge 1 commit intomainfrom
feature/inspect-layout-and-semantics-tab

Conversation

@TBSten
Copy link
Owner

@TBSten TBSten commented Dec 31, 2025

Summary

  • JVM(デスクトップ)向けに Layout and Semantics インスペクタータブを追加
  • ComposeのセマンティクスツリーをUIで確認できる機能
  • LocalComposeWindow CompositionLocal を追加し、ComposeWindowへのアクセスを提供

Changes

  • LayoutAndSemanticsTab: セマンティクスノードをツリー表示するタブ
  • DefaultInspectorTabs: プラットフォーム別に expect/actual で切り替え
  • otherDesktop ソースセット追加(Android/iOS/Web向けフォールバック)

Test plan

  • JVMでPreviewLabGalleryを起動し、Layout and Semanticsタブが表示されることを確認
  • セマンティクスツリーが正しく表示されることを確認
  • Android/iOS/Webではタブが表示されないことを確認

🤖 Generated with Claude Code

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 LayoutAndSemanticsTab for JVM that displays the semantics tree with merged/unmerged toggle and auto-refresh
  • Introduced platform-specific DefaultInspectorTabs using expect/actual with SharedDefaultInspectorTabs as the base
  • Created LocalComposeWindow CompositionLocal 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 }}" }
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ""}".

Suggested change
?.let { cd -> "${cd.take(30)}${"...".takeIf { cd.length > 30 }}" }
?.let { cd -> "${cd.take(30)}${if (cd.length > 30) "..." else ""}" }

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +66
LaunchedEffect(state) {
while (true) {
state.value = getContentSemanticsNodeTree(semanticsOwner, useUnmerged)
delay(0.2.seconds)
}
}
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
import me.tbsten.compose.preview.lab.ui.generated.resources.icon_refresh
import org.jetbrains.compose.resources.painterResource

@ExperimentalComposePreviewLabApi
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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."

Suggested change
@ExperimentalComposePreviewLabApi
@ExperimentalComposePreviewLabApi
/**
* Built-in Layout and Semantics tab that displays the semantics tree for JVM/Desktop platforms.
*/

Copilot uses AI. Check for mistakes.

internal actual val DefaultInspectorTabs: List<InspectorTab> = SharedDefaultInspectorTabs + listOf(
LayoutAndSemanticsTab,
).also { println("jvm DefaultInspectorTabs") }
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug println statement should be removed before merging. This appears to be leftover debug code that will clutter console output in production.

Suggested change
).also { println("jvm DefaultInspectorTabs") }
)

Copilot uses AI. Check for mistakes.
.find { it.config.getOrNull(SemanticsProperties.TestTag) == "PreviewLab.content" }
}

fun SemanticsNode.find(includeSelf: Boolean = true, block: (SemanticsNode) -> Boolean): SemanticsNode? {
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
fun SemanticsNode.find(includeSelf: Boolean = true, block: (SemanticsNode) -> Boolean): SemanticsNode? {
private fun SemanticsNode.find(includeSelf: Boolean = true, block: (SemanticsNode) -> Boolean): SemanticsNode? {

Copilot uses AI. Check for mistakes.
Comment on lines +164 to +169
val isRoot = isRoot || node.isRoot

Column {
Row {
val text: String =
"(Root)".takeIf { isRoot }
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'.

Suggested change
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 }

Copilot uses AI. Check for mistakes.

Column(Modifier.padding(start = 20.dp)) {
node.children.forEach { child ->

Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change

Copilot uses AI. Check for mistakes.
internal val SharedDefaultInspectorTabs = listOf(
InspectorTab.Fields,
InspectorTab.Events,
).also { println("commonMain SharedDefaultInspectorTabs") }
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug println statement should be removed before merging. This appears to be leftover debug code that will clutter console output in production.

Suggested change
).also { println("commonMain SharedDefaultInspectorTabs") }
)

Copilot uses AI. Check for mistakes.
Comment on lines +159 to +162
currentDeps: Int,
maxDeps: Int,
) {
if (maxDeps < currentDeps) return
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
currentDeps: Int,
maxDeps: Int,
) {
if (maxDeps < currentDeps) return
currentDepth: Int,
maxDepth: Int,
) {
if (maxDepth < currentDepth) return

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant