Skip to content

Instantly share code, notes, and snippets.

@YektaDev
Last active October 20, 2025 17:00
Show Gist options
  • Select an option

  • Save YektaDev/1789622f81d4ba0cc8cc18c32a238fb1 to your computer and use it in GitHub Desktop.

Select an option

Save YektaDev/1789622f81d4ba0cc8cc18c32a238fb1 to your computer and use it in GitHub Desktop.
A Composable Box that draws bounds and sizes of its children.
/**
* InspectorBox.kt - https://gist.github.com/YektaDev/1789622f81d4ba0cc8cc18c32a238fb1
*
* MIT License
*
* Copyright (c) 2025 Ali Khaleqi Yekta
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Applier
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composition
import androidx.compose.runtime.currentComposer
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCompositionContext
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.BlendMode.Companion.Difference
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.layout.LayoutInfo
import androidx.compose.ui.layout.boundsInRoot
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.font.FontFamily.Companion.Monospace
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlin.math.roundToInt
// There are certain known problems with the implementation:
// 1. The bounds do not always represent the exact latest position and size of the composables.
// 2. Not all UI elements are captured (e.g., items within a LazyColumn are not captured).
// 3. The layoutInfoList is never cleared.
// [ Contributions to fix these are welcome. ]
private val borderColor = Color.White.copy(alpha = .85f)
private val borderStroke = Stroke(1f)
private val defaultTextStyle = TextStyle.Default.copy(Color.White, 12f.sp, fontFamily = Monospace)
@Composable
fun InspectorBox(
contentAlignment: Alignment = Alignment.TopStart,
style: TextStyle = defaultTextStyle,
content: @Composable BoxScope.() -> Unit,
) {
val composer = currentComposer
val context = rememberCompositionContext()
remember {
val layoutInfoList = mutableStateListOf<LayoutInfo>()
val observingApplier = ObserverApplier(composer.applier) { layoutInfoList += it }
val allBounds = mutableListOf<Rect>()
Composition(observingApplier, context).setContent {
val measurer = rememberTextMeasurer()
Box(
modifier = Modifier
.drawWithCache {
allBounds.clear()
val dpPx = 1f.dp.toPx()
for (info in layoutInfoList) {
if (info.isPlaced && info.isAttached) {
allBounds += info.coordinates.boundsInRoot()
}
}
onDrawWithContent {
drawContent()
allBounds.forEach { bounds ->
drawRect(
blendMode = Difference,
color = borderColor,
topLeft = bounds.topLeft,
size = bounds.size,
style = borderStroke,
)
drawText(
textMeasurer = measurer,
text = "${bounds.width.roundToInt()}×${bounds.height.roundToInt()}",
topLeft = Offset(bounds.left + dpPx, bounds.top + dpPx),
softWrap = false,
style = style,
)
}
}
},
contentAlignment = contentAlignment,
content = content,
)
}
}
}
private class ObserverApplier<T>(
private val wrapped: Applier<T>,
private val onLayoutInfoInserted: (LayoutInfo) -> Unit,
) : Applier<T> by wrapped {
override fun insertTopDown(index: Int, instance: T) {
wrapped.insertTopDown(index, instance)
if (instance is LayoutInfo) onLayoutInfoInserted(instance)
}
override fun insertBottomUp(index: Int, instance: T) {
wrapped.insertBottomUp(index, instance)
if (instance is LayoutInfo) onLayoutInfoInserted(instance)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment