Add additional inspectables

Summary:
This change adds support for more inspectables and also introduces more complex types to be used as a value.

This become specially useful for more complex yet primitive types like coordinate, size, bounds, etc.

Reviewed By: LukeDefeo

Differential Revision: D40307885

fbshipit-source-id: 125e832f06d6b31f56eb5405182d1c0d61388930
This commit is contained in:
Lorenzo Blasa
2022-10-18 04:30:51 -07:00
committed by Facebook GitHub Bot
parent f7a624a143
commit 0572808f1a
27 changed files with 417 additions and 132 deletions

View File

@@ -8,13 +8,13 @@
package com.facebook.flipper.plugins.uidebugger.litho.descriptors package com.facebook.flipper.plugins.uidebugger.litho.descriptors
import android.graphics.Bitmap import android.graphics.Bitmap
import com.facebook.flipper.plugins.uidebugger.common.InspectableObject
import com.facebook.flipper.plugins.uidebugger.descriptors.BaseTags import com.facebook.flipper.plugins.uidebugger.descriptors.BaseTags
import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister
import com.facebook.flipper.plugins.uidebugger.descriptors.NodeDescriptor import com.facebook.flipper.plugins.uidebugger.descriptors.NodeDescriptor
import com.facebook.flipper.plugins.uidebugger.descriptors.OffsetChild import com.facebook.flipper.plugins.uidebugger.descriptors.OffsetChild
import com.facebook.flipper.plugins.uidebugger.litho.LithoTag import com.facebook.flipper.plugins.uidebugger.litho.LithoTag
import com.facebook.flipper.plugins.uidebugger.model.Bounds import com.facebook.flipper.plugins.uidebugger.model.Bounds
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.litho.DebugComponent import com.facebook.litho.DebugComponent
class DebugComponentDescriptor(val register: DescriptorRegister) : NodeDescriptor<DebugComponent> { class DebugComponentDescriptor(val register: DescriptorRegister) : NodeDescriptor<DebugComponent> {

View File

@@ -7,11 +7,11 @@
package com.facebook.flipper.plugins.uidebugger.litho.descriptors package com.facebook.flipper.plugins.uidebugger.litho.descriptors
import com.facebook.flipper.plugins.uidebugger.common.Inspectable
import com.facebook.flipper.plugins.uidebugger.common.InspectableObject
import com.facebook.flipper.plugins.uidebugger.common.InspectableValue
import com.facebook.flipper.plugins.uidebugger.descriptors.ChainedDescriptor import com.facebook.flipper.plugins.uidebugger.descriptors.ChainedDescriptor
import com.facebook.flipper.plugins.uidebugger.descriptors.SectionName import com.facebook.flipper.plugins.uidebugger.descriptors.SectionName
import com.facebook.flipper.plugins.uidebugger.model.Inspectable
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.InspectableValue
import com.facebook.litho.DebugComponent import com.facebook.litho.DebugComponent
import com.facebook.litho.LithoView import com.facebook.litho.LithoView
@@ -19,7 +19,7 @@ object LithoViewDescriptor : ChainedDescriptor<LithoView>() {
override fun onGetName(node: LithoView): String = node.javaClass.simpleName override fun onGetName(node: LithoView): String = node.javaClass.simpleName
override fun onGetChildren(node: LithoView): List<Any>? { override fun onGetChildren(node: LithoView): List<Any> {
val result = mutableListOf<Any>() val result = mutableListOf<Any>()
val debugComponent = DebugComponent.getRootInstance(node) val debugComponent = DebugComponent.getRootInstance(node)
if (debugComponent != null) { if (debugComponent != null) {

View File

@@ -8,9 +8,9 @@
package com.facebook.flipper.plugins.uidebugger.litho.descriptors package com.facebook.flipper.plugins.uidebugger.litho.descriptors
import android.graphics.Bitmap import android.graphics.Bitmap
import com.facebook.flipper.plugins.uidebugger.common.InspectableObject
import com.facebook.flipper.plugins.uidebugger.descriptors.* import com.facebook.flipper.plugins.uidebugger.descriptors.*
import com.facebook.flipper.plugins.uidebugger.model.Bounds import com.facebook.flipper.plugins.uidebugger.model.Bounds
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
/** a drawable or view that is mounted, along with the correct descriptor */ /** a drawable or view that is mounted, along with the correct descriptor */
class MountedObject(val obj: Any, val descriptor: NodeDescriptor<Any>) class MountedObject(val obj: Any, val descriptor: NodeDescriptor<Any>)
@@ -19,14 +19,16 @@ object MountedObjectDescriptor : NodeDescriptor<MountedObject> {
override fun getBounds(node: MountedObject): Bounds? { override fun getBounds(node: MountedObject): Bounds? {
val bounds = node.descriptor.getBounds(node.obj) val bounds = node.descriptor.getBounds(node.obj)
bounds?.let { b ->
/** /**
* When we ask android for the bounds the x,y offset is w.r.t to the nearest android parent view * When we ask android for the bounds the x,y offset is w.r.t to the nearest android parent
* group. From UI debuggers perspective using the raw android offset will double the total * view group. From UI debuggers perspective using the raw android offset will double the
* offset of this native view as the offset is included by the litho components between the * total offset of this native view as the offset is included by the litho components between
* mounted view and its native parent * the mounted view and its native parent
*/ */
return bounds?.copy(x = 0, y = 0) return Bounds(0, 0, b.width, b.height)
}
return null
} }
override fun getName(node: MountedObject): String = node.descriptor.getName(node.obj) override fun getName(node: MountedObject): String = node.descriptor.getName(node.obj)

View File

@@ -7,11 +7,11 @@
package com.facebook.flipper.plugins.uidebugger.litho.descriptors package com.facebook.flipper.plugins.uidebugger.litho.descriptors
import com.facebook.flipper.plugins.uidebugger.common.Inspectable
import com.facebook.flipper.plugins.uidebugger.common.InspectableObject
import com.facebook.flipper.plugins.uidebugger.common.InspectableValue
import com.facebook.flipper.plugins.uidebugger.descriptors.ChainedDescriptor import com.facebook.flipper.plugins.uidebugger.descriptors.ChainedDescriptor
import com.facebook.flipper.plugins.uidebugger.descriptors.SectionName import com.facebook.flipper.plugins.uidebugger.descriptors.SectionName
import com.facebook.flipper.plugins.uidebugger.model.Inspectable
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.InspectableValue
import com.facebook.litho.widget.TextDrawable import com.facebook.litho.widget.TextDrawable
object TextDrawableDescriptor : ChainedDescriptor<TextDrawable>() { object TextDrawableDescriptor : ChainedDescriptor<TextDrawable>() {

View File

@@ -9,6 +9,8 @@ package com.facebook.flipper.plugins.uidebugger.common
import android.util.Log import android.util.Log
import com.facebook.flipper.plugins.uidebugger.LogTag import com.facebook.flipper.plugins.uidebugger.LogTag
import com.facebook.flipper.plugins.uidebugger.model.Enumeration
import com.facebook.flipper.plugins.uidebugger.model.InspectableValue
// Maintains 2 way mapping between some enum value and a readable string representation // Maintains 2 way mapping between some enum value and a readable string representation
open class EnumMapping<T>(private val mapping: Map<String, T>) { open class EnumMapping<T>(private val mapping: Map<String, T>) {
@@ -32,7 +34,7 @@ open class EnumMapping<T>(private val mapping: Map<String, T>) {
} }
fun toInspectable(value: T, mutable: Boolean): InspectableValue.Enum { fun toInspectable(value: T, mutable: Boolean): InspectableValue.Enum {
return InspectableValue.Enum(EnumData(mapping.keys, getStringRepresentation(value)), mutable) return InspectableValue.Enum(Enumeration(mapping.keys, getStringRepresentation(value)), mutable)
} }
companion object { companion object {
const val NoMapping = "__UNKNOWN_ENUM_VALUE__" const val NoMapping = "__UNKNOWN_ENUM_VALUE__"

View File

@@ -8,8 +8,8 @@
package com.facebook.flipper.plugins.uidebugger.descriptors package com.facebook.flipper.plugins.uidebugger.descriptors
import android.app.Activity import android.app.Activity
import com.facebook.flipper.plugins.uidebugger.common.InspectableObject
import com.facebook.flipper.plugins.uidebugger.core.FragmentTracker import com.facebook.flipper.plugins.uidebugger.core.FragmentTracker
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
object ActivityDescriptor : ChainedDescriptor<Activity>() { object ActivityDescriptor : ChainedDescriptor<Activity>() {

View File

@@ -20,7 +20,7 @@ object ApplicationRefDescriptor : ChainedDescriptor<ApplicationRef>() {
} }
override fun onGetBounds(node: ApplicationRef): Bounds { override fun onGetBounds(node: ApplicationRef): Bounds {
val displayMetrics = Resources.getSystem().getDisplayMetrics() val displayMetrics = Resources.getSystem().displayMetrics
return Bounds(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels) return Bounds(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels)
} }

View File

@@ -8,8 +8,8 @@
package com.facebook.flipper.plugins.uidebugger.descriptors package com.facebook.flipper.plugins.uidebugger.descriptors
import android.graphics.Bitmap import android.graphics.Bitmap
import com.facebook.flipper.plugins.uidebugger.common.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.Bounds import com.facebook.flipper.plugins.uidebugger.model.Bounds
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
/** /**
* A chained descriptor is a special type of descriptor that models the inheritance hierarchy in * A chained descriptor is a special type of descriptor that models the inheritance hierarchy in

View File

@@ -9,9 +9,10 @@ package com.facebook.flipper.plugins.uidebugger.descriptors
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Build import android.os.Build
import com.facebook.flipper.plugins.uidebugger.common.Inspectable import com.facebook.flipper.plugins.uidebugger.model.Color
import com.facebook.flipper.plugins.uidebugger.common.InspectableObject import com.facebook.flipper.plugins.uidebugger.model.Inspectable
import com.facebook.flipper.plugins.uidebugger.common.InspectableValue import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.InspectableValue
object ColorDrawableDescriptor : ChainedDescriptor<ColorDrawable>() { object ColorDrawableDescriptor : ChainedDescriptor<ColorDrawable>() {
@@ -22,9 +23,8 @@ object ColorDrawableDescriptor : ChainedDescriptor<ColorDrawable>() {
attributeSections: MutableMap<SectionName, InspectableObject> attributeSections: MutableMap<SectionName, InspectableObject>
) { ) {
val props = mutableMapOf<String, Inspectable>() val props = mutableMapOf<String, Inspectable>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
props.put("color", InspectableValue.Color(node.color, mutable = true)) props["color"] = InspectableValue.Color(Color.fromColor(node.color), mutable = true)
} }
attributeSections["ColorDrawable"] = InspectableObject(props.toMap()) attributeSections["ColorDrawable"] = InspectableObject(props.toMap())

View File

@@ -13,6 +13,7 @@ import android.graphics.drawable.Drawable
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.Window import android.view.Window
import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.viewpager.widget.ViewPager import androidx.viewpager.widget.ViewPager
import com.facebook.flipper.plugins.uidebugger.common.UIDebuggerException import com.facebook.flipper.plugins.uidebugger.common.UIDebuggerException
@@ -32,6 +33,7 @@ class DescriptorRegister {
mapping.register(ViewGroup::class.java, ViewGroupDescriptor) mapping.register(ViewGroup::class.java, ViewGroupDescriptor)
mapping.register(View::class.java, ViewDescriptor) mapping.register(View::class.java, ViewDescriptor)
mapping.register(TextView::class.java, TextViewDescriptor) mapping.register(TextView::class.java, TextViewDescriptor)
mapping.register(ImageView::class.java, ImageViewDescriptor)
mapping.register(ViewPager::class.java, ViewPagerDescriptor) mapping.register(ViewPager::class.java, ViewPagerDescriptor)
mapping.register(Drawable::class.java, DrawableDescriptor) mapping.register(Drawable::class.java, DrawableDescriptor)
mapping.register(ColorDrawable::class.java, ColorDrawableDescriptor) mapping.register(ColorDrawable::class.java, ColorDrawableDescriptor)

View File

@@ -9,10 +9,7 @@ package com.facebook.flipper.plugins.uidebugger.descriptors
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Build import android.os.Build
import com.facebook.flipper.plugins.uidebugger.common.Inspectable import com.facebook.flipper.plugins.uidebugger.model.*
import com.facebook.flipper.plugins.uidebugger.common.InspectableObject
import com.facebook.flipper.plugins.uidebugger.common.InspectableValue
import com.facebook.flipper.plugins.uidebugger.model.Bounds
object DrawableDescriptor : ChainedDescriptor<Drawable>() { object DrawableDescriptor : ChainedDescriptor<Drawable>() {
override fun onGetName(node: Drawable): String = node.javaClass.simpleName override fun onGetName(node: Drawable): String = node.javaClass.simpleName
@@ -24,13 +21,15 @@ object DrawableDescriptor : ChainedDescriptor<Drawable>() {
node: Drawable, node: Drawable,
attributeSections: MutableMap<SectionName, InspectableObject> attributeSections: MutableMap<SectionName, InspectableObject>
) { ) {
val props = mutableMapOf<String, Inspectable>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
props.put("alpha", InspectableValue.Number(node.getAlpha(), true)) val props = mutableMapOf<String, Inspectable>()
} props["alpha"] = InspectableValue.Number(node.alpha, true)
attributeSections["Drawable"] = InspectableObject(props.toMap()) val bounds = node.bounds
props["bounds"] = InspectableValue.SpaceBox(SpaceBox.fromRect(bounds))
attributeSections["Drawable"] = InspectableObject(props.toMap())
}
} }
override fun onGetTags(node: Drawable): Set<String> = BaseTags.NativeAndroid override fun onGetTags(node: Drawable): Set<String> = BaseTags.NativeAndroid

View File

@@ -7,7 +7,11 @@
package com.facebook.flipper.plugins.uidebugger.descriptors package com.facebook.flipper.plugins.uidebugger.descriptors
import com.facebook.flipper.plugins.uidebugger.common.InspectableObject import android.os.Build
import android.os.Bundle
import com.facebook.flipper.plugins.uidebugger.model.Inspectable
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.InspectableValue
object FragmentFrameworkDescriptor : ChainedDescriptor<android.app.Fragment>() { object FragmentFrameworkDescriptor : ChainedDescriptor<android.app.Fragment>() {
@@ -21,5 +25,20 @@ object FragmentFrameworkDescriptor : ChainedDescriptor<android.app.Fragment>() {
override fun onGetData( override fun onGetData(
node: android.app.Fragment, node: android.app.Fragment,
attributeSections: MutableMap<String, InspectableObject> attributeSections: MutableMap<String, InspectableObject>
) {} ) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
val args: Bundle = node.arguments
val props = mutableMapOf<String, Inspectable>()
for (key in args.keySet()) {
when (val value = args[key]) {
is Number -> props[key] = InspectableValue.Number(value)
is Boolean -> props[key] = InspectableValue.Boolean(value)
is String -> props[key] = InspectableValue.Text(value)
}
}
attributeSections["Fragment"] = InspectableObject(props.toMap())
}
}
} }

View File

@@ -7,7 +7,9 @@
package com.facebook.flipper.plugins.uidebugger.descriptors package com.facebook.flipper.plugins.uidebugger.descriptors
import com.facebook.flipper.plugins.uidebugger.common.InspectableObject import com.facebook.flipper.plugins.uidebugger.model.Inspectable
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.InspectableValue
object FragmentSupportDescriptor : ChainedDescriptor<androidx.fragment.app.Fragment>() { object FragmentSupportDescriptor : ChainedDescriptor<androidx.fragment.app.Fragment>() {
@@ -21,5 +23,18 @@ object FragmentSupportDescriptor : ChainedDescriptor<androidx.fragment.app.Fragm
override fun onGetData( override fun onGetData(
node: androidx.fragment.app.Fragment, node: androidx.fragment.app.Fragment,
attributeSections: MutableMap<String, InspectableObject> attributeSections: MutableMap<String, InspectableObject>
) {} ) {
val args = node.arguments
args?.let { bundle ->
val props = mutableMapOf<String, Inspectable>()
for (key in bundle.keySet()) {
when (val value = bundle[key]) {
is Number -> props[key] = InspectableValue.Number(value)
is Boolean -> props[key] = InspectableValue.Boolean(value)
is String -> props[key] = InspectableValue.Text(value)
}
}
attributeSections["Fragment"] = InspectableObject(props.toMap())
}
}
} }

View File

@@ -0,0 +1,42 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.flipper.plugins.uidebugger.descriptors
import android.widget.ImageView
import android.widget.ImageView.ScaleType
import com.facebook.flipper.plugins.uidebugger.common.EnumMapping
import com.facebook.flipper.plugins.uidebugger.model.Inspectable
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
object ImageViewDescriptor : ChainedDescriptor<ImageView>() {
override fun onGetName(node: ImageView): String = node.javaClass.simpleName
override fun onGetData(
node: ImageView,
attributeSections: MutableMap<SectionName, InspectableObject>
) {
val props = mutableMapOf<String, Inspectable>()
props["scaleType"] = scaleTypeMapping.toInspectable(node.scaleType, true)
attributeSections["ImageView"] = InspectableObject(props)
}
private val scaleTypeMapping: EnumMapping<ScaleType> =
object :
EnumMapping<ScaleType>(
mapOf(
"CENTER" to ScaleType.CENTER,
"CENTER_CROP" to ScaleType.CENTER_CROP,
"CENTER_INSIDE" to ScaleType.CENTER_INSIDE,
"FIT_CENTER" to ScaleType.FIT_CENTER,
"FIT_END" to ScaleType.FIT_END,
"FIT_START" to ScaleType.FIT_START,
"FIT_XY" to ScaleType.FIT_XY,
"MATRIX" to ScaleType.MATRIX,
)) {}
}

View File

@@ -8,8 +8,8 @@
package com.facebook.flipper.plugins.uidebugger.descriptors package com.facebook.flipper.plugins.uidebugger.descriptors
import android.graphics.Bitmap import android.graphics.Bitmap
import com.facebook.flipper.plugins.uidebugger.common.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.Bounds import com.facebook.flipper.plugins.uidebugger.model.Bounds
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
/* /*
Descriptors are an extension point used during traversal to extract data out of arbitrary Descriptors are an extension point used during traversal to extract data out of arbitrary

View File

@@ -8,8 +8,8 @@
package com.facebook.flipper.plugins.uidebugger.descriptors package com.facebook.flipper.plugins.uidebugger.descriptors
import android.graphics.Bitmap import android.graphics.Bitmap
import com.facebook.flipper.plugins.uidebugger.common.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.Bounds import com.facebook.flipper.plugins.uidebugger.model.Bounds
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
object ObjectDescriptor : NodeDescriptor<Any> { object ObjectDescriptor : NodeDescriptor<Any> {

View File

@@ -8,8 +8,8 @@
package com.facebook.flipper.plugins.uidebugger.descriptors package com.facebook.flipper.plugins.uidebugger.descriptors
import android.graphics.Bitmap import android.graphics.Bitmap
import com.facebook.flipper.plugins.uidebugger.common.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.Bounds import com.facebook.flipper.plugins.uidebugger.model.Bounds
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
/** a drawable or view that is mounted, along with the correct descriptor */ /** a drawable or view that is mounted, along with the correct descriptor */
class OffsetChild(val child: Any, val descriptor: NodeDescriptor<Any>, val x: Int, val y: Int) { class OffsetChild(val child: Any, val descriptor: NodeDescriptor<Any>, val x: Int, val y: Int) {
@@ -22,7 +22,10 @@ object OffsetChildDescriptor : NodeDescriptor<OffsetChild> {
override fun getBounds(node: OffsetChild): Bounds? { override fun getBounds(node: OffsetChild): Bounds? {
val bounds = node.descriptor.getBounds(node.child) val bounds = node.descriptor.getBounds(node.child)
return bounds?.copy(x = node.x, y = node.y) bounds?.let { b ->
return Bounds(node.x, node.y, b.width, b.height)
}
return null
} }
override fun getName(node: OffsetChild): String = node.descriptor.getName(node.child) override fun getName(node: OffsetChild): String = node.descriptor.getName(node.child)

View File

@@ -9,9 +9,10 @@ package com.facebook.flipper.plugins.uidebugger.descriptors
import android.os.Build import android.os.Build
import android.widget.TextView import android.widget.TextView
import com.facebook.flipper.plugins.uidebugger.common.Inspectable import com.facebook.flipper.plugins.uidebugger.model.Color
import com.facebook.flipper.plugins.uidebugger.common.InspectableObject import com.facebook.flipper.plugins.uidebugger.model.Inspectable
import com.facebook.flipper.plugins.uidebugger.common.InspectableValue import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.InspectableValue
object TextViewDescriptor : ChainedDescriptor<TextView>() { object TextViewDescriptor : ChainedDescriptor<TextView>() {
@@ -26,7 +27,8 @@ object TextViewDescriptor : ChainedDescriptor<TextView>() {
mutableMapOf<String, Inspectable>( mutableMapOf<String, Inspectable>(
"text" to InspectableValue.Text(node.text.toString(), false), "text" to InspectableValue.Text(node.text.toString(), false),
"textSize" to InspectableValue.Number(node.textSize, false), "textSize" to InspectableValue.Number(node.textSize, false),
"textColor" to InspectableValue.Color(node.getTextColors().getDefaultColor(), false)) "textColor" to
InspectableValue.Color(Color.fromColor(node.textColors.defaultColor), false))
val typeface = node.typeface val typeface = node.typeface
if (typeface != null) { if (typeface != null) {
@@ -50,6 +52,6 @@ object TextViewDescriptor : ChainedDescriptor<TextView>() {
props["maxWidth"] = InspectableValue.Number(node.maxWidth, false) props["maxWidth"] = InspectableValue.Number(node.maxWidth, false)
} }
attributeSections.put("TextView", InspectableObject(props)) attributeSections["TextView"] = InspectableObject(props)
} }
} }

View File

@@ -20,8 +20,9 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.LinearLayout import android.widget.LinearLayout
import com.facebook.flipper.plugins.uidebugger.common.* import com.facebook.flipper.plugins.uidebugger.common.BitmapPool
import com.facebook.flipper.plugins.uidebugger.model.Bounds import com.facebook.flipper.plugins.uidebugger.common.EnumMapping
import com.facebook.flipper.plugins.uidebugger.model.*
import com.facebook.flipper.plugins.uidebugger.util.ResourcesUtil import com.facebook.flipper.plugins.uidebugger.util.ResourcesUtil
import java.lang.reflect.Field import java.lang.reflect.Field
@@ -47,14 +48,16 @@ object ViewDescriptor : ChainedDescriptor<View>() {
) { ) {
val props = mutableMapOf<String, Inspectable>() val props = mutableMapOf<String, Inspectable>()
props["height"] = InspectableValue.Number(node.height, mutable = true) props["size"] = InspectableValue.Size(Size(node.width, node.height), mutable = true)
props["width"] = InspectableValue.Number(node.width, mutable = true)
props["alpha"] = InspectableValue.Number(node.alpha, mutable = true) props["alpha"] = InspectableValue.Number(node.alpha, mutable = true)
props["visibility"] = VisibilityMapping.toInspectable(node.visibility, mutable = false) props["visibility"] = VisibilityMapping.toInspectable(node.visibility, mutable = false)
fromDrawable(node.background)?.let { props["background"] = it } fromDrawable(node.background)?.let { background -> props["background"] = background }
node.tag
?.let { InspectableValue.fromAny(it, mutable = false) }
?.let { tag -> props.put("tag", tag) }
node.tag?.let { InspectableValue.fromAny(it, mutable = false) }?.let { props.put("tag", it) }
props["keyedTags"] = InspectableObject(getViewTags(node)) props["keyedTags"] = InspectableObject(getViewTags(node))
props["layoutParams"] = getLayoutParams(node) props["layoutParams"] = getLayoutParams(node)
props["state"] = props["state"] =
@@ -66,46 +69,20 @@ object ViewDescriptor : ChainedDescriptor<View>() {
"selected" to InspectableValue.Boolean(node.isSelected, mutable = false))) "selected" to InspectableValue.Boolean(node.isSelected, mutable = false)))
props["bounds"] = props["bounds"] =
InspectableObject( InspectableValue.SpaceBox(SpaceBox(node.top, node.right, node.bottom, node.left))
mapOf<String, Inspectable>(
"left" to InspectableValue.Number(node.left, mutable = true),
"right" to InspectableValue.Number(node.right, mutable = true),
"top" to InspectableValue.Number(node.top, mutable = true),
"bottom" to InspectableValue.Number(node.bottom, mutable = true)))
props["padding"] = props["padding"] =
InspectableObject( InspectableValue.SpaceBox(
mapOf<String, Inspectable>( SpaceBox(node.paddingTop, node.paddingRight, node.paddingBottom, node.paddingLeft))
"left" to InspectableValue.Number(node.paddingLeft, mutable = true),
"right" to InspectableValue.Number(node.paddingRight, mutable = true),
"top" to InspectableValue.Number(node.paddingTop, mutable = true),
"bottom" to InspectableValue.Number(node.paddingBottom, mutable = true)))
props["rotation"] = props["rotation"] =
InspectableObject( InspectableValue.Coordinate3D(Coordinate3D(node.rotationX, node.rotationY, node.rotation))
mapOf<String, Inspectable>( props["scale"] = InspectableValue.Coordinate(Coordinate(node.scaleX, node.scaleY))
"x" to InspectableValue.Number(node.rotationX, mutable = true), props["pivot"] = InspectableValue.Coordinate(Coordinate(node.pivotX, node.pivotY))
"y" to InspectableValue.Number(node.rotationY, mutable = true),
"z" to InspectableValue.Number(node.rotation, mutable = true)))
props["scale"] =
InspectableObject(
mapOf(
"x" to InspectableValue.Number(node.scaleX, mutable = true),
"y" to InspectableValue.Number(node.scaleY, mutable = true)))
props["pivot"] =
InspectableObject(
mapOf(
"x" to InspectableValue.Number(node.pivotX, mutable = true),
"y" to InspectableValue.Number(node.pivotY, mutable = true)))
val positionOnScreen = IntArray(2) val positionOnScreen = IntArray(2)
node.getLocationOnScreen(positionOnScreen) node.getLocationOnScreen(positionOnScreen)
props["globalPosition"] = props["globalPosition"] =
InspectableObject( InspectableValue.Coordinate(Coordinate(positionOnScreen[0], positionOnScreen[1]))
mapOf(
"x" to InspectableValue.Number(positionOnScreen[0], mutable = false),
"y" to InspectableValue.Number(positionOnScreen[1], mutable = false)))
val localVisible = Rect() val localVisible = Rect()
node.getLocalVisibleRect(localVisible) node.getLocalVisibleRect(localVisible)
@@ -113,12 +90,39 @@ object ViewDescriptor : ChainedDescriptor<View>() {
props["localVisible"] = props["localVisible"] =
InspectableObject( InspectableObject(
mapOf( mapOf(
"x" to InspectableValue.Number(localVisible.left, mutable = true), "position" to InspectableValue.Coordinate(Coordinate(localVisible.left, node.top)),
"y" to InspectableValue.Number(node.top, mutable = true), "size" to InspectableValue.Size(Size(node.width, node.height))),
"width" to InspectableValue.Number(node.width, mutable = true),
"height" to InspectableValue.Number(node.height, mutable = true)),
) )
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
props["layoutDirection"] = LayoutDirectionMapping.toInspectable(node.layoutDirection, false)
props["textDirection"] = TextDirectionMapping.toInspectable(node.textDirection, false)
props["textAlignment"] = TextAlignmentMapping.toInspectable(node.textAlignment, false)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
props["elevation"] = InspectableValue.Number(node.elevation)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
props["translation"] =
InspectableValue.Coordinate3D(
Coordinate3D(node.translationX, node.translationY, node.translationZ))
} else {
props["translation"] =
InspectableValue.Coordinate(Coordinate(node.translationX, node.translationY))
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
props["position"] = InspectableValue.Coordinate3D(Coordinate3D(node.x, node.y, node.z))
} else {
props["position"] = InspectableValue.Coordinate(Coordinate(node.x, node.y))
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
fromDrawable(node.foreground)?.let { foreground -> props["foreground"] = foreground }
}
attributeSections["View"] = InspectableObject(props.toMap()) attributeSections["View"] = InspectableObject(props.toMap())
} }
@@ -148,7 +152,7 @@ object ViewDescriptor : ChainedDescriptor<View>() {
private fun fromDrawable(d: Drawable?): Inspectable? { private fun fromDrawable(d: Drawable?): Inspectable? {
return if (d is ColorDrawable) { return if (d is ColorDrawable) {
InspectableValue.Color(d.color, mutable = false) InspectableValue.Color(Color.fromColor(d.color), mutable = false)
} else null } else null
} }
@@ -160,15 +164,13 @@ object ViewDescriptor : ChainedDescriptor<View>() {
params["height"] = LayoutParamsMapping.toInspectable(layoutParams.height, mutable = true) params["height"] = LayoutParamsMapping.toInspectable(layoutParams.height, mutable = true)
if (layoutParams is ViewGroup.MarginLayoutParams) { if (layoutParams is ViewGroup.MarginLayoutParams) {
val margin = params["margin"] =
InspectableObject( InspectableValue.SpaceBox(
mapOf<String, Inspectable>( SpaceBox(
"left" to InspectableValue.Number(layoutParams.leftMargin, mutable = true), layoutParams.topMargin,
"top" to InspectableValue.Number(layoutParams.topMargin, mutable = true), layoutParams.rightMargin,
"right" to InspectableValue.Number(layoutParams.rightMargin, mutable = true), layoutParams.bottomMargin,
"bottom" to InspectableValue.Number(layoutParams.bottomMargin, mutable = true))) layoutParams.leftMargin))
params["margin"] = margin
} }
if (layoutParams is FrameLayout.LayoutParams) { if (layoutParams is FrameLayout.LayoutParams) {
params["gravity"] = GravityMapping.toInspectable(layoutParams.gravity, mutable = true) params["gravity"] = GravityMapping.toInspectable(layoutParams.gravity, mutable = true)

View File

@@ -11,8 +11,9 @@ import android.os.Build
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.ViewGroupCompat import androidx.core.view.ViewGroupCompat
import com.facebook.flipper.plugins.uidebugger.common.* import com.facebook.flipper.plugins.uidebugger.common.EnumMapping
import com.facebook.flipper.plugins.uidebugger.core.FragmentTracker import com.facebook.flipper.plugins.uidebugger.core.FragmentTracker
import com.facebook.flipper.plugins.uidebugger.model.*
object ViewGroupDescriptor : ChainedDescriptor<ViewGroup>() { object ViewGroupDescriptor : ChainedDescriptor<ViewGroup>() {

View File

@@ -8,8 +8,8 @@
package com.facebook.flipper.plugins.uidebugger.descriptors package com.facebook.flipper.plugins.uidebugger.descriptors
import androidx.viewpager.widget.ViewPager import androidx.viewpager.widget.ViewPager
import com.facebook.flipper.plugins.uidebugger.common.InspectableObject import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.common.InspectableValue import com.facebook.flipper.plugins.uidebugger.model.InspectableValue
object ViewPagerDescriptor : ChainedDescriptor<ViewPager>() { object ViewPagerDescriptor : ChainedDescriptor<ViewPager>() {
@@ -25,6 +25,6 @@ object ViewPagerDescriptor : ChainedDescriptor<ViewPager>() {
InspectableObject( InspectableObject(
mapOf("currentItemIndex" to InspectableValue.Number(node.currentItem, false))) mapOf("currentItemIndex" to InspectableValue.Number(node.currentItem, false)))
attributeSections.put("ViewPager", props) attributeSections["ViewPager"] = props
} }
} }

View File

@@ -7,13 +7,99 @@
package com.facebook.flipper.plugins.uidebugger.descriptors package com.facebook.flipper.plugins.uidebugger.descriptors
import android.annotation.SuppressLint
import android.util.TypedValue
import android.view.Window import android.view.Window
import com.facebook.flipper.plugins.uidebugger.model.Color
import com.facebook.flipper.plugins.uidebugger.model.Inspectable
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.InspectableValue
import java.lang.reflect.Field
object WindowDescriptor : ChainedDescriptor<Window>() { object WindowDescriptor : ChainedDescriptor<Window>() {
private var internalRStyleableClass: Class<*>? = null
private var internalRStyleableFields: Array<Field>? = null
private var internalRStyleableWindowField: Field? = null
private var internalRStyleable: Any? = null
override fun onGetName(node: Window): String { override fun onGetName(node: Window): String {
return node.javaClass.simpleName return node.javaClass.simpleName
} }
override fun onGetChildren(node: Window): List<Any> = listOf(node.decorView) override fun onGetChildren(node: Window): List<Any> = listOf(node.decorView)
@SuppressLint("PrivateApi")
override fun onGetData(
node: Window,
attributeSections: MutableMap<SectionName, InspectableObject>
) {
try {
if (internalRStyleableClass == null) {
internalRStyleableClass = Class.forName("com.android.internal.R\$styleable")
internalRStyleableClass?.let { clazz ->
internalRStyleable = clazz.newInstance()
internalRStyleableFields = clazz.declaredFields
internalRStyleableWindowField = clazz.getDeclaredField("Window")
internalRStyleableWindowField?.isAccessible = true
}
}
} catch (e: Exception) {
return
}
internalRStyleableWindowField?.let { field ->
val windowStyleable = field[internalRStyleable] as IntArray? ?: return
val indexToName: MutableMap<Int, String> = mutableMapOf()
internalRStyleableFields?.forEach { f ->
if (f.name.startsWith("Window_") && f.type == Int::class.javaPrimitiveType) {
indexToName[f.getInt(internalRStyleable)] = f.name
}
}
val props = mutableMapOf<String, Inspectable>()
val typedValue = TypedValue()
for ((index, attr) in windowStyleable.withIndex()) {
val fieldName = indexToName[index] ?: continue
if (node.context.theme.resolveAttribute(attr, typedValue, true)) {
// Strip 'Windows_' (length: 7) from the name.
val name = fieldName.substring(7)
when (typedValue.type) {
TypedValue.TYPE_STRING ->
props[name] = InspectableValue.Text(typedValue.string.toString())
TypedValue.TYPE_INT_BOOLEAN ->
props[name] = InspectableValue.Boolean(typedValue.data != 0)
TypedValue.TYPE_INT_HEX ->
props[name] = InspectableValue.Text("0x" + Integer.toHexString(typedValue.data))
TypedValue.TYPE_FLOAT ->
props[name] =
InspectableValue.Number(java.lang.Float.intBitsToFloat(typedValue.data))
TypedValue.TYPE_REFERENCE -> {
val resId = typedValue.data
if (resId != 0) {
props[name] = InspectableValue.Text(node.context.resources.getResourceName(resId))
}
}
else -> {
if (typedValue.type >= TypedValue.TYPE_FIRST_COLOR_INT &&
typedValue.type <= TypedValue.TYPE_LAST_COLOR_INT) {
val hexColor = "#" + Integer.toHexString(typedValue.data)
val color = android.graphics.Color.parseColor(hexColor)
props[name] = InspectableValue.Color(Color.fromColor(color))
} else if (typedValue.type >= TypedValue.TYPE_FIRST_INT &&
typedValue.type <= TypedValue.TYPE_LAST_INT) {
props[name] = InspectableValue.Number(typedValue.data)
}
}
}
}
}
attributeSections["Window"] = InspectableObject(props.toMap())
}
}
} }

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
package com.facebook.flipper.plugins.uidebugger.common package com.facebook.flipper.plugins.uidebugger.model
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
@@ -21,13 +21,15 @@ sealed class Inspectable {
abstract val mutable: kotlin.Boolean abstract val mutable: kotlin.Boolean
} }
// mutable here means you can add/remove items, for native android this should probably be false // In this context, mutable means you can add/remove items,
// for native android this should probably be false.
@SerialName("array") @SerialName("array")
@Serializable @Serializable
data class InspectableArray(val items: List<Inspectable>, override val mutable: Boolean = false) : data class InspectableArray(val items: List<Inspectable>, override val mutable: Boolean = false) :
Inspectable() Inspectable()
// mutable here means you can add / remove keys, for native android this should probably be false // In this context, mutable means you can add / remove keys,
// for native android this should probably be false.
@SerialName("object") @SerialName("object")
@Serializable @Serializable
data class InspectableObject( data class InspectableObject(
@@ -40,34 +42,75 @@ sealed class InspectableValue : Inspectable() {
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
@SerialName("text") @SerialName("text")
class Text(val value: String, override val mutable: kotlin.Boolean) : InspectableValue() class Text(val value: String, override val mutable: kotlin.Boolean = false) : InspectableValue()
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
@SerialName("boolean") @SerialName("boolean")
class Boolean(val value: kotlin.Boolean, override val mutable: kotlin.Boolean) : class Boolean(val value: kotlin.Boolean, override val mutable: kotlin.Boolean = false) :
InspectableValue() InspectableValue()
@SerialName("number") @SerialName("number")
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
data class Number( data class Number(
@Serializable(with = NumberSerializer::class) val value: kotlin.Number, @Serializable(with = NumberSerializer::class) val value: kotlin.Number,
override val mutable: kotlin.Boolean override val mutable: kotlin.Boolean = false
) : InspectableValue() ) : InspectableValue()
@SerialName("color") @SerialName("color")
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
data class Color(val value: Int, override val mutable: kotlin.Boolean) : InspectableValue() data class Color(
val value: com.facebook.flipper.plugins.uidebugger.model.Color,
override val mutable: kotlin.Boolean = false
) : InspectableValue()
@SerialName("coordinate")
@kotlinx.serialization.Serializable
data class Coordinate(
val value: com.facebook.flipper.plugins.uidebugger.model.Coordinate,
override val mutable: kotlin.Boolean = false
) : InspectableValue()
@SerialName("coordinate3d")
@kotlinx.serialization.Serializable
data class Coordinate3D(
val value: com.facebook.flipper.plugins.uidebugger.model.Coordinate3D,
override val mutable: kotlin.Boolean = false
) : InspectableValue()
@SerialName("size")
@kotlinx.serialization.Serializable
data class Size(
val value: com.facebook.flipper.plugins.uidebugger.model.Size,
override val mutable: kotlin.Boolean = false
) : InspectableValue()
@SerialName("bounds")
@kotlinx.serialization.Serializable
data class Bounds(
val value: com.facebook.flipper.plugins.uidebugger.model.Bounds,
override val mutable: kotlin.Boolean = false
) : InspectableValue()
@SerialName("space")
@kotlinx.serialization.Serializable
data class SpaceBox(
val value: com.facebook.flipper.plugins.uidebugger.model.SpaceBox,
override val mutable: kotlin.Boolean = false
) : InspectableValue()
@SerialName("enum") @SerialName("enum")
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
data class Enum(val value: EnumData, override val mutable: kotlin.Boolean) : InspectableValue() data class Enum(
val value: com.facebook.flipper.plugins.uidebugger.model.Enumeration,
override val mutable: kotlin.Boolean = false
) : InspectableValue()
companion object { companion object {
/** /**
* Will attempt to convert Any ref to a suitable primitive inspectable value. Only use if you * Will attempt to convert Any ref to a suitable primitive inspectable value. Only use if you
* are dealing with an Any / object type. Prefer the specific contructors * are dealing with an Any / object type. Prefer the specific constructors
*/ */
fun fromAny(any: Any, mutable: kotlin.Boolean): Inspectable? { fun fromAny(any: Any, mutable: kotlin.Boolean = false): Inspectable? {
return when (any) { return when (any) {
is kotlin.Number -> InspectableValue.Number(any, mutable) is kotlin.Number -> InspectableValue.Number(any, mutable)
is kotlin.Boolean -> InspectableValue.Boolean(any, mutable) is kotlin.Boolean -> InspectableValue.Boolean(any, mutable)
@@ -78,8 +121,6 @@ sealed class InspectableValue : Inspectable() {
} }
} }
@kotlinx.serialization.Serializable data class EnumData(val values: Set<String>, val value: String)
object NumberSerializer : KSerializer<Number> { object NumberSerializer : KSerializer<Number> {
override val descriptor: SerialDescriptor = override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("com.meta.NumberSerializer", PrimitiveKind.DOUBLE) PrimitiveSerialDescriptor("com.meta.NumberSerializer", PrimitiveKind.DOUBLE)

View File

@@ -7,8 +7,6 @@
package com.facebook.flipper.plugins.uidebugger.model package com.facebook.flipper.plugins.uidebugger.model
import android.graphics.Rect
import com.facebook.flipper.plugins.uidebugger.common.InspectableObject
import com.facebook.flipper.plugins.uidebugger.descriptors.Id import com.facebook.flipper.plugins.uidebugger.descriptors.Id
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
@@ -21,12 +19,3 @@ data class Node(
val children: List<Id>, val children: List<Id>,
val activeChild: Id?, val activeChild: Id?,
) )
@kotlinx.serialization.Serializable
data class Bounds(val x: Int, val y: Int, val width: Int, val height: Int) {
companion object {
fun fromRect(rect: Rect): Bounds {
return Bounds(rect.left, rect.top, rect.width(), rect.height())
}
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.flipper.plugins.uidebugger.model
import android.graphics.Rect
import kotlinx.serialization.Serializable
@kotlinx.serialization.Serializable
data class Bounds(val x: Int, val y: Int, val width: Int, val height: Int) {
companion object {
fun fromRect(rect: Rect): Bounds {
return Bounds(rect.left, rect.top, rect.width(), rect.height())
}
}
}
@kotlinx.serialization.Serializable
data class SpaceBox(val top: Int, val right: Int, val bottom: Int, val left: Int) {
companion object {
fun fromRect(rect: Rect): SpaceBox {
return SpaceBox(rect.top, rect.right, rect.bottom, rect.left)
}
}
}
@kotlinx.serialization.Serializable
data class Color(val r: Int, val g: Int, val b: Int, val alpha: Int) {
companion object {
fun fromColor(color: Int): Color {
val alpha: Int = (color shr 24) and 0xFF / 255
val red: Int = (color shr 16) and 0xFF
val green: Int = (color shr 8) and 0xFF
val blue: Int = (color shr 0) and 0xFF
return Color(red, green, blue, alpha)
}
fun fromColor(color: android.graphics.Color): Color {
return fromColor(color.toArgb())
}
}
}
@kotlinx.serialization.Serializable
data class Coordinate(
@Serializable(with = NumberSerializer::class) val x: Number,
@Serializable(with = NumberSerializer::class) val y: Number
) {}
@kotlinx.serialization.Serializable
data class Coordinate3D(
@Serializable(with = NumberSerializer::class) val x: Number,
@Serializable(with = NumberSerializer::class) val y: Number,
@Serializable(with = NumberSerializer::class) val z: Number
) {}
@kotlinx.serialization.Serializable
data class Size(
@Serializable(with = NumberSerializer::class) val width: Number,
@Serializable(with = NumberSerializer::class) val height: Number
) {}
@kotlinx.serialization.Serializable
data class Enumeration(val values: Set<String>, val value: String)

View File

@@ -8,9 +8,9 @@
package com.facebook.flipper.plugins.uidebugger package com.facebook.flipper.plugins.uidebugger
import android.view.View import android.view.View
import com.facebook.flipper.plugins.uidebugger.common.EnumData
import com.facebook.flipper.plugins.uidebugger.common.EnumMapping import com.facebook.flipper.plugins.uidebugger.common.EnumMapping
import com.facebook.flipper.plugins.uidebugger.common.InspectableValue import com.facebook.flipper.plugins.uidebugger.model.Enumeration
import com.facebook.flipper.plugins.uidebugger.model.InspectableValue
import org.hamcrest.CoreMatchers.* import org.hamcrest.CoreMatchers.*
import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.MatcherAssert.assertThat
import org.junit.Test import org.junit.Test
@@ -42,6 +42,6 @@ class EnumMappingTest {
visibility.toInspectable(View.GONE, true), visibility.toInspectable(View.GONE, true),
equalTo( equalTo(
InspectableValue.Enum( InspectableValue.Enum(
EnumData(setOf("VISIBLE", "INVISIBLE", "GONE"), "GONE"), mutable = true))) Enumeration(setOf("VISIBLE", "INVISIBLE", "GONE"), "GONE"), mutable = true)))
} }
} }

View File

@@ -51,6 +51,13 @@ export type Bounds = {
height: number; height: number;
}; };
export type Color = {
r: number;
g: number;
b: number;
alpha: number;
};
export type Snapshot = string; export type Snapshot = string;
export type Id = number; export type Id = number;
@@ -76,7 +83,13 @@ export type InspectableNumber = {
export type InspectableColor = { export type InspectableColor = {
type: 'number'; type: 'number';
value: number; value: Color;
mutable: boolean;
};
export type InspectableBounds = {
type: 'bounds';
value: Bounds;
mutable: boolean; mutable: boolean;
}; };