LazyValueVisualizer.kt
src/jvmMain/kotlin/afield/ui/visualization/LazyValueVisualizer.kt · 1050 lines
package and imports — 50 lines, from line 1
@file:afield.runtime.completion.CompletionCategory("Visualization")
package afield.ui.visualization
import afield.app.protocol.put
import afield.data.table.div
import afield.data.table.minus
import afield.data.table.plus
import afield.data.table.rem
import afield.data.table.times
import afield.document.graph.box
import afield.document.graph.nested
import afield.document.graph.nestedIn
import afield.document.graph.offline
import afield.runtime.completion.CompletionCategory
import afield.runtime.execution.plus
import afield.runtime.lifecycle.lazyVar
import afield.runtime.lifecycle.loop
import afield.runtime.lifecycle.plus
import afield.runtime.lifecycle.rem
import afield.runtime.lifecycle.then
import afield.ui.notation.center
import afield.ui.notation.string
import afield.ui.notation.with
import afield.ui.output.column
import afield.ui.output.tree
import afield.audio.synth.*
import afield.data.table.cell
import afield.data.table.get
import afield.document.graph.Box
import afield.document.workspace.Entry
import afield.graphics.vulkan.*
import afield.media.stream.*
import afield.media.video.*
import afield.runtime.lifecycle.end
import afield.runtime.lifecycle.map
import afield.tools.docs.format
import afield.ui.output.FieldMarkdown
import afield.app.server.Browsable
import afield.app.server.Inspector
import afield.math.units.Quantity
import afield.math.units.formatExpression
import afield.math.units.formatQuantityDisplay
import eu.sirotin.kotunil.core.Expression
import java.nio.FloatBuffer
import java.nio.IntBuffer
import java.util.LinkedHashMap
import java.util.UUID
import kotlin.math.abs
The lazy half of value display: rather than inline a large structure, show one summary line plus links into the server-side browsers.
LazyValueVisualizer.visualize — reached in a box as h2 — passes anything small or
unrecognised straight to ValueVisualizer. For maps, lists, arrays and FloatBuffer /
IntBuffer it instead retains the value under a token and emits a summary, outliner and
columns links, and an embedded /h2-explorer iframe. Those tokens live in an in-memory
retention table (10 minutes, 256 entries), so the links only resolve against the running server
and go stale after that.
The private Browsable wrappers behind those routes page rather than copy: children are grouped
into range-labelled directories of at most 96 entries, and a map with more than 20,000 entries is
iterated on demand instead of snapshotted. registerH2Value adds a per-type adapter returning an
H2ValueSnapshot when a type should be summarised or unwrapped differently, and
lazyInlineExpanderHtml is the inline "… +N more" link ValueVisualizer emits for elided
content.
private const val H2_MAX_CHILDREN_PER_DIR = 96
private const val H2_LIST_LEAF_SIZE = 128
private const val H2_MAP_LEAF_SIZE = 96
private const val H2_FLOAT_ROWS_LEAF_SIZE = 128
private const val H2_IFRAME_HEIGHT_PX = 253
private const val H2_EMBED_EXPLORER_PATH = "/h2-explorer"
private const val H2_OUTLINER_PATH = "/tree"
private const val H2_COLUMNS_PATH = "/browse"
private const val INLINE_EXPAND_PATH = "/lazy-value-fragment"
private const val H2_TOKEN_MAX = 256
private const val H2_TOKEN_TTL_MS = 10 * 60 * 1000L
private const val H2_MAP_SNAPSHOT_THRESHOLD = 20_000
private const val H2_OUTPUT_VIEW_THRESHOLD = 32
data class H2ValueSnapshot
What a .h2 adapter returns: the value to explore in place of the one it was handed, and the wording to show for
it.
All three fields are advisory. An adapter that only wants to rename something passes the original value straight
back and fills in title; one that only wants to redirect the explorer fills in value and leaves the wording to
be generated.
data class H2ValueSnapshot(
/**
* What the explorer pages through in place of the value the adapter was given.
*
* An adapter that only wants to supply a label hands the original object back here. The substitution is applied
* only when this is a *different* instance, and the adapter's substitute is never passed back through the
* registry, so two adapters that substituted for each other could not spin.
*/
val value: Any?,
/**
* The one-line description shown above the explorer, used verbatim in place of the generated
* `Type (size=N)` line.
*/
val summary: String? = null,
/**
* The label the explored value carries in the browsers.
*
* Taken unchanged at the top level. A value reached from inside another structure keeps the caller's label too
* and reads as `[0] — Box neighbour`, because the position alone does not say what is there and the adapter's
* own title does not say where it sits. Falls back to [summary] when null.
*/
val title: String? = null
)
object H2ValueRegistry
The table of per-type .h2 adapters, and the rule that decides which one serves a given value.
Adapters go in by exact class through register, but come out by a walk up the superclass chain that checks each
class's whole interface closure on the way, so registering against an interface covers everything that implements
it. This is one of the two registries a type outside Field reaches the value display through; ValuePrinterRegistry
is the other, and resolves identically.
object H2ValueRegistry {
private val adapters = mutableMapOf<Class<*>, (Any) -> H2ValueSnapshot>()
fun <T : Any> register(clazz: Class<T>, adapter: (T) -> H2ValueSnapshot) {
Records adapter against clazz itself, replacing any adapter already registered for that exact class.
The erased form, for a Class only known at runtime. From Kotlin, call registerH2Value instead and let the
reified type parameter name the class.
fun <T : Any> register(clazz: Class<T>, adapter: (T) -> H2ValueSnapshot) {
@Suppress("UNCHECKED_CAST")
adapters[clazz] = adapter as (Any) -> H2ValueSnapshot
}
internal fun adapt(value: Any): H2ValueSnapshot? = adapterFor(value)?.invoke(value)
The .h2 adapter registered for value's type, or null.
Same resolution rule, and same fix, as ValuePrinterRegistry.getPrinter: the superclass chain most-derived first, and at each step that class's whole interface closure rather than only the interfaces the runtime class declares. Kept identical, because the two registries are the pair of extension points a third-party type reaches Field through, and having them disagree about what "registered for this type" means is the bug twice.
internal fun adapterFor(value: Any): ((Any) -> H2ValueSnapshot)? {
The adapter registered for value's type, without running it.
Split from adapt for the explorer's drill: deciding whether a row can be entered asks this
question once per visible row, and an adapter is arbitrary user code — Box's walks a
property bag, DataFrame's wraps a frame. Answering "is one registered" by invoking it would
do that work up to a windowful of times to render chevrons nobody clicked.
internal fun adapterFor(value: Any): ((Any) -> H2ValueSnapshot)? {
var clazz: Class<*>? = value::class.java
while (clazz != null) {
adapters[clazz]?.let { return it }
findInInterfaceClosure(clazz.interfaces)?.let { return it }
clazz = clazz.superclass
}
return null
}
Breadth-first over roots and everything they extend, so declared interfaces win over theirs.
source — 46 lines, from line 169
private fun findInInterfaceClosure(roots: Array<Class<*>>): ((Any) -> H2ValueSnapshot)? {
val queue = ArrayDeque(roots.toList())
val seen = HashSet<Class<*>>(roots.toList())
while (queue.isNotEmpty()) {
val iface = queue.removeFirst()
adapters[iface]?.let { return it }
iface.interfaces.forEach { if (seen.add(it)) queue.addLast(it) }
}
return null
}
}
private object H2TokenRetention {
private data class Entry(val raw: Any?, val createdAtMs: Long)
private val retained = LinkedHashMap<String, Entry>()
@Synchronized
fun remember(token: String, raw: Any?) {
val now = System.currentTimeMillis()
retained[token] = Entry(raw, now)
cleanup(now)
}
@Synchronized
fun fetch(token: String): Any? {
val now = System.currentTimeMillis()
cleanup(now)
return retained[token]?.raw
}
@Synchronized
private fun cleanup(now: Long) {
val it = retained.entries.iterator()
while (it.hasNext()) {
val (_, entry) = it.next()
if (now - entry.createdAtMs > H2_TOKEN_TTL_MS) {
it.remove()
}
}
while (retained.size > H2_TOKEN_MAX) {
val eldestKey = retained.entries.firstOrNull()?.key ?: break
retained.remove(eldestKey)
}
}
}
object LazyValueVisualizer
The renderer behind .h2, and the only public entry point into it.
Everything else in this file either feeds visualize (the registry, the adapters) or serves the routes the panel it emits links to.
object LazyValueVisualizer {
fun visualize(value: Any?, appendable: Boolean = false): String
Renders value as the .h2 panel: a summary line, outliner and columns links into the server-side
browsers, and an embedded explorer iframe.
A value with no registered adapter that is also not a collection, array, buffer or explorable object is handed
to ValueVisualizer instead, so .h2 is never worse than .h for something small. The value itself is
retained under a fresh token for the links to resolve against, which is why they stop working once the server
restarts or the retention window passes. appendable adds the afield-appendable-output marker class, which
the browser reads to let a second panel join the first rather than replace it.
fun visualize(value: Any?, appendable: Boolean = false): String {
val adapted = value?.let { H2ValueRegistry.adapt(it) }
if (adapted == null && !isExplorable(value)) return ValueVisualizer.visualize(value, appendable = appendable)
val rawValue = adapted?.value ?: value
// `wrapUnadapted`, not `wrapForInspector`: `adapted` above is this value's registry lookup,
// and `rawValue` is already the substitute. The front door would only run the registry a
// second time over that substitute.
val wrapped = wrapUnadapted(rawValue, title = adapted?.title ?: "value")
val token = "h2-${UUID.randomUUID()}"
H2TokenRetention.remember(token, rawValue)
val outlinerPath = Inspector.linkPathFor(wrapped ?: "null", browsePath = H2_OUTLINER_PATH, uid = token)
val columnsPath = Inspector.linkPathFor(wrapped ?: "null", browsePath = H2_COLUMNS_PATH, uid = token)
val embeddedPath = "$H2_EMBED_EXPLORER_PATH?root=$token"
val ownerUid = Box.context.get()?.id
val titleForViews = adapted?.title?.ifBlank { null } ?: "Value"
return buildString {
append("<div class='value-viz-h2")
if (appendable) append(" afield-appendable-output")
append("' style='font-family:\"Iosevka Web\",monospace;font-size:12px;color:#ccc;line-height:1.35;display:flex;flex-direction:column;align-items:stretch;width:100%;height:100%;min-height:0;box-sizing:border-box;'>")
append("<div style='display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:6px;'>")
append("<span style='color:#9cf;'>")
append(escapeHtml(adapted?.summary ?: summary(rawValue)))
append("</span>")
append(outputAreaLinkHtml(outlinerPath, "outliner", "#8cf", ownerUid, titleForViews))
append(outputAreaLinkHtml(columnsPath, "columns", "#777", ownerUid, titleForViews))
append("</div>")
append("<iframe data-afield-autosize='off' src='")
append(escapeHtmlAttr(embeddedPath))
append("' loading='lazy' style='position:static;display:block;width:100%;height:${H2_IFRAME_HEIGHT_PX}px;flex:1 1 auto;min-height:0;border:1px solid #333;border-radius:4px;background:#000;'></iframe>")
append("</div>")
}
}
One of the panel's two view links, opening in the box's own output area rather than a browser tab.
afieldOpenLazyValueInOutputArea (jsMain/Outputs.kt) has done this since before .h2 grew
an explorer: it reads the three data-afield-view-*/data-afield-box-uid attributes and hands
them to upsertLazyValueViewer, which opens a disclosure in the output area with the iframe
inside it. The inline .h expander lazyInlineExpanderHtml renders its own "columns" link
exactly this way. These two links were the only place in the file that did not, and a .h2
panel is the surface most likely to be clicked, so a value explored from a box left the box.
target="_blank" stays as the href fallback, which is what happens when the page's JS is
absent — a docs capture, a saved page, an offline viewer. The handler returns false, so the
navigation only occurs when nothing claimed the click.
ownerUid is null outside a box's execution context. There is no output area to open into
then, so the link degrades to the old behaviour rather than becoming inert.
source — 57 lines, from line 286
private fun outputAreaLinkHtml(
path: String,
label: String,
colour: String,
ownerUid: String?,
viewTitle: String
): String = buildString {
append("<a href='")
append(escapeHtmlAttr(path))
if (!ownerUid.isNullOrBlank()) {
append("' data-afield-view-src='")
append(escapeHtmlAttr(path))
append("' data-afield-box-uid='")
append(escapeHtmlAttr(ownerUid))
append("' data-afield-view-title='")
append(escapeHtmlAttr("$viewTitle — $label"))
append("' onclick='window.afieldOpenLazyValueInOutputArea && window.afieldOpenLazyValueInOutputArea(this); return false;'")
append(" style='color:$colour;text-decoration:none;cursor:pointer;'>")
} else {
append("' target='_blank' style='color:$colour;text-decoration:none;'>")
}
append(label)
append("</a>")
}
private fun summary(value: Any?): String = when (value) {
null -> "null"
is Map<*, *> -> "${value::class.simpleName ?: "Map"} (size=${value.size})"
is List<*> -> unitCollectionSummary(value, value::class.simpleName ?: "List")
?: "${value::class.simpleName ?: "List"} (size=${value.size})"
is Array<*> -> unitCollectionSummary(value.asList(), value::class.simpleName ?: "Array")
?: "${value::class.simpleName ?: "Array"} (size=${value.size})"
is Collection<*> -> "${value::class.simpleName ?: "Collection"} (size=${value.size})"
is IntArray -> "IntArray (size=${value.size})"
is LongArray -> "LongArray (size=${value.size})"
is FloatArray -> "FloatArray (size=${value.size})"
is DoubleArray -> "DoubleArray (size=${value.size})"
is FloatBuffer -> "FloatBuffer (pos=${value.position()}, limit=${value.limit()}, capacity=${value.capacity()})"
is IntBuffer -> "IntBuffer (pos=${value.position()}, limit=${value.limit()}, capacity=${value.capacity()})"
// A reflected domain object gets a property count, because "Foo" alone tells a person
// nothing about whether the tree below it is worth opening.
else -> if (H2Reflect.isExplorableObject(value)) H2Reflect.summaryLine(value)
else value::class.simpleName ?: value::class.java.simpleName
}
private fun unitCollectionSummary(values: List<*>, kind: String): String? {
if (values.isEmpty()) return null
val symbols = values.mapNotNull {
when (it) {
is Quantity -> formatQuantityDisplay(it).second
is Expression -> formatExpression(it).second
else -> return null
}
}.distinct()
val suffix = if (symbols.size == 1 && symbols.single().isNotBlank()) " · ${symbols.single()}" else " · mixed units"
return "$kind (size=${values.size})$suffix"
}
Whether .h2 engages for value at all, as opposed to handing it straight to .h.
The union of two rules, kept separate because they are settled by different arguments:
isHeavyStructure— a collection or buffer. There is no size threshold here, and that is the point. This gate used to be strictly narrower than the machinery behind it: it admittedIntArraybut notByteArray,Listbut notSet, whilewrapForInspectorbelow andmakeSourceinh2explorer.ktboth already rendered all of them. The gate's job is to admit exactly what the three downstream surfaces can draw, so the rule is now "anything with indexable elements", and any future addition belongs in all three places or none.- H2Reflect.isExplorableObject — a domain object with properties worth a tree. See that
function for why the property floor is 2 rather than
.h's elision point.
private fun isExplorable(value: Any?): Boolean =
isHeavyStructure(value) || H2Reflect.isExplorableObject(value)
internal fun canExplore(value: Any?): Boolean
Whether .h2 would open an explorer for value rather than hand it to .h.
The same question the opening guard in visualize asks, exposed because the explorer's drill
has to ask it per row: a row can be entered exactly when the value behind it is one .h2
would have explored had someone typed .h2 on it directly. Deliberately this function and
not a structural copy of it in h2explorer.kt — there are already three type ladders over
this value space and a fourth that disagreed with them would put a chevron on a row that
opens empty, or withhold one from a row that would have opened fine.
internal fun canExplore(value: Any?): Boolean =
value != null && (H2ValueRegistry.adapterFor(value) != null || isExplorable(value))
A collection, array or buffer — something whose elements are the content.
Collection rather than List covers a Set, an ArrayDeque and a Queue; the price is
that wrapForInspector and makeSource snapshot a non-List collection with toList(),
because both surfaces address rows by index and only a List can answer that without a copy.
A List therefore keeps its no-copy path deliberately, ahead of the general case.
private fun isHeavyStructure(value: Any?): Boolean {
return value is Map<*, *> ||
value is Collection<*> ||
value is Array<*> ||
value is ByteArray ||
value is ShortArray ||
value is IntArray ||
value is LongArray ||
value is FloatArray ||
value is DoubleArray ||
value is BooleanArray ||
value is CharArray ||
value is FloatBuffer ||
value is IntBuffer
}
}
internal fun rememberLazyValueToken(value: Any?): String
internal fun rememberLazyValueToken(value: Any?): String {
val token = "lazy-${UUID.randomUUID()}"
H2TokenRetention.remember(token, value)
return token
}
internal fun h2ExplorerPathForToken(token: String): String
internal fun h2ExplorerPathForToken(token: String): String = "$H2_EMBED_EXPLORER_PATH?root=$token"
private fun collectionSize(value: Any?): Int? = when (value) {
is Collection<*> -> value.size
is Map<*, *> -> value.size
is Array<*> -> value.size
is ByteArray -> value.size
is ShortArray -> value.size
is IntArray -> value.size
is LongArray -> value.size
is FloatArray -> value.size
is DoubleArray -> value.size
is BooleanArray -> value.size
is CharArray -> value.size
is FloatBuffer -> value.limit()
is IntBuffer -> value.limit()
else -> null
}
internal fun shouldOfferH2OutputAreaViewer(value: Any?): Boolean
internal fun shouldOfferH2OutputAreaViewer(value: Any?): Boolean {
val size = collectionSize(value) ?: return false
if (size < H2_OUTPUT_VIEW_THRESHOLD) return false
return when (value) {
is List<*>, is Array<*>,
is ByteArray, is ShortArray, is IntArray, is LongArray,
is FloatArray, is DoubleArray, is BooleanArray, is CharArray,
is FloatBuffer, is IntBuffer -> true
else -> false
}
}
internal fun lazyInlineExpanderHtml
internal fun lazyInlineExpanderHtml(
value: Any?,
label: String,
kind: String,
offset: Int,
depth: Int = 0,
title: String? = null,
ownerUid: String? = Box.context.get()?.id,
offerOutputAreaViewer: Boolean = shouldOfferH2OutputAreaViewer(value)
): String {
val token = rememberLazyValueToken(value)
val expandPath = "$INLINE_EXPAND_PATH?root=$token&kind=$kind&offset=$offset&depth=$depth"
val outputAreaPath = if (offerOutputAreaViewer) h2ExplorerPathForToken(token) else null
val outputAreaTitle = title?.ifBlank { null } ?: "Columns"
return buildString {
append("<span class='afield-lazy-inline'>")
append("<a href='../' class='afield-lazy-inline-link' data-afield-lazy-src='")
append(escapeHtmlAttr(expandPath))
append("' onclick='window.afieldExpandLazyValue && window.afieldExpandLazyValue(this); return false;'")
append(" style='color:#aaa;text-decoration:underline;text-underline-offset:2px;text-decoration-thickness:1px;cursor:pointer;'>")
append(escapeHtml(label))
append("</a>")
if (outputAreaPath != null && !ownerUid.isNullOrBlank()) {
append("<span style='color:#666;'> </span>")
append("<a href='../' class='afield-lazy-inline-link afield-lazy-inline-side-link'")
append(" data-afield-view-src='")
append(escapeHtmlAttr(outputAreaPath))
append("' data-afield-box-uid='")
append(escapeHtmlAttr(ownerUid))
append("' data-afield-view-title='")
append(escapeHtmlAttr(outputAreaTitle))
append("' onclick='window.afieldOpenLazyValueInOutputArea && window.afieldOpenLazyValueInOutputArea(this); return false;'")
append(" style='color:#777;text-decoration:underline;text-underline-offset:2px;text-decoration-thickness:1px;cursor:pointer;'>")
append("columns")
append("</a>")
}
append("</span>")
}
}
internal fun wrapForInspector(value: Any?, title: String? = null): Any?
The Browsable (or bare value) that /tree and /browse show for value.
internal rather than private because H2Reflect's reflective tree re-enters it for each
property: a property holding a list must get the same paging a top-level list gets, or the two
routes disagree about the same object depending on how you reached it.
A registered adapter wins at every depth, not only at the root
LazyValueVisualizer.visualize consults H2ValueRegistry for the value a person typed .h2 on,
and until this function did too, nothing consulted it again. That made the registry a root-only
mechanism, which is not what it reads like at its call sites, and it broke the one invariant the
Box adapter exists to hold: the adapter reports liveKi rather than reading Box.ki — a
lazyVar that compiles a K2 REPL engine on read — and then hands box.properties to the map
pager. children, parents, nested and nestedIn are Delegation properties, so they sit in
that bag holding raw Box objects, the pager resolves an element through this function, and the
reflective default took whatever the arms below did not claim. A neighbouring box reached at
properties -> children -> [0] was therefore reflected and its ki read. H2Reflect's own KDoc
states why that must not happen; the guard simply did not survive one hop.
Adapters are also the better rendering wherever they apply, so this is a fix and a feature: a
nested DataFrame or FrameBufferGroup now pages through its own adapter instead of being
reflected into position/limit.
The dispatch is deliberately one hop, not a loop. An adapter's substitute is handed to
wrapUnadapted, which never re-consults the registry, so a pair of adapters that substituted for
each other cannot spin. H2ValueSnapshot.value also defaults to substituting nothing, so an
adapter written only to supply a summary hands back the object it was given — the !== check is
what keeps that case a leaf rather than an infinite regress. All three adapters in the tree today
substitute (Box -> map, DataFrame -> list, FrameBufferGroup -> array), so both guards are
load-bearing for the next adapter rather than for these.
internal fun wrapForInspector(value: Any?, title: String? = null): Any? {
val adapted = value?.let { H2ValueRegistry.adapt(it) }
if (adapted != null && adapted.value !== value) {
return wrapUnadapted(adapted.value, adaptedLabel(title, adapted))
}
return wrapUnadapted(value, title)
}
The node label for a value an adapter substituted.
Same label — summary shape H2ReflectiveBrowsable.heading() uses, and for the same reason: a
nested box reached at [0] reads [0] — Box neighbour, because [0] alone does not say which
box and the adapter's own title does not say where it sits. A caller with no label, or the top
level's placeholder "value", takes the adapter's title unchanged.
private fun adaptedLabel(callerTitle: String?, adapted: H2ValueSnapshot): String? {
val own = adapted.title ?: adapted.summary
if (own.isNullOrBlank()) return callerTitle
if (callerTitle.isNullOrBlank() || callerTitle == "value") return own
return "$callerTitle — $own"
}
wrapForInspector with the registry already consulted.
Split out so that the adapter lookup happens exactly once per value: LazyValueVisualizer.visualize has already adapted the root before it wraps, and calling the front door there would run the registry over the substitute for nothing.
source — 342 lines, from line 540
private fun wrapUnadapted(value: Any?, title: String? = null): Any? {
return when (value) {
null -> null
is Quantity -> QuantityBrowsable(value)
is Expression -> UnitExpressionBrowsable(value)
is PagedListBrowsable -> value
is PagedMapBrowsable -> value
is FloatBufferBrowsable -> value
is IntBufferBrowsable -> value
is Map<*, *> -> PagedMapBrowsable.forMap(value, title)
is List<*> -> PagedListBrowsable(value, title = title)
is Array<*> -> PagedListBrowsable(value.asList(), title = title)
is ByteArray -> PagedListBrowsable(value.asList(), title = title)
is ShortArray -> PagedListBrowsable(value.asList(), title = title)
is IntArray -> IntBufferBrowsable(IntBuffer.wrap(value), title = title)
is LongArray -> PagedListBrowsable(value.asList(), title = title)
is FloatArray -> FloatBufferBrowsable(FloatBuffer.wrap(value), title = title)
is DoubleArray -> PagedListBrowsable(value.asList(), title = title)
is BooleanArray -> PagedListBrowsable(value.asList(), title = title)
is CharArray -> PagedListBrowsable(value.asList(), title = title)
is FloatBuffer -> FloatBufferBrowsable(value, title = title)
is IntBuffer -> IntBufferBrowsable(value, title = title)
// After `List`, so a list keeps the no-copy path: a `Set` or a `Queue` is snapshotted
// because the pager addresses rows by index and only a `List` can do that in place.
is Collection<*> -> PagedListBrowsable(value.toList(), title = title)
// The reflective default, reached only for types with no registered adapter — `wrapForInspector`
// has already taken those. Returns null for anything `H2Reflect` declines — a scalar, a
// platform type, a value whose `.h` rendering is already the whole story — and then the raw
// value falls through to the Inspector's leaf renderer, exactly as before this existed.
else -> H2Reflect.browsableFor(value, title) ?: value
}
}
private class QuantityBrowsable(private val quantity: Quantity) : Browsable {
override fun heading(): String = quantity.toString()
override fun dir(): Map<String, Any?> = linkedMapOf(
"display value" to quantity.displayValue,
"display unit" to quantity.displaySymbol,
"SI value" to quantity.value,
"SI unit" to quantity.unitSymbols.trim()
)
override fun html(): String = ValueVisualizer.visualize(quantity)
}
private class UnitExpressionBrowsable(private val expression: Expression) : Browsable {
private val formatted by lazy { formatExpression(expression) }
override fun heading(): String = formatted.let { (value, symbol) ->
if (symbol.isBlank()) value.toString() else "$value $symbol"
}
override fun dir(): Map<String, Any?> = linkedMapOf(
"SI value" to expression.value,
"SI unit" to expression.unitSymbols().trim()
)
override fun html(): String = ValueVisualizer.visualize(expression)
}
private fun rangeLabel(start: Int, endExclusive: Int): String {
val last = endExclusive - 1
return if (start > last) "[empty]" else "[$start..$last]"
}
private fun ceilDiv(value: Int, by: Int): Int {
if (value <= 0) return 0
return (value + by - 1) / by
}
private fun computeChildSpan(count: Int, leafSpan: Int): Int {
var span = leafSpan.coerceAtLeast(1)
while (ceilDiv(count, span) > H2_MAX_CHILDREN_PER_DIR) {
val next = span * H2_MAX_CHILDREN_PER_DIR
if (next <= span) {
return count.coerceAtLeast(leafSpan)
}
span = next
}
return span
}
private fun clip(text: String, maxChars: Int = 64): String {
if (text.length <= maxChars) return text
return text.take(maxChars - 3) + "..."
}
private fun keyPreview(key: Any?): String {
if (key == null) return "null"
return clip(key.toString().replace('\n', ' '))
}
private fun fmtFloat(n: Float): String {
return when {
n.isNaN() -> "NaN"
n.isInfinite() -> if (n > 0f) "Infinity" else "-Infinity"
abs(n) < 0.001f && n != 0f -> "%.2e".format(n)
abs(n) > 10000f -> "%.2e".format(n)
else -> "%.4f".format(n).trimEnd('0').trimEnd('.')
}
}
private class PagedListBrowsable(
private val list: List<*>,
private val title: String? = null,
private val start: Int = 0,
private val endExclusive: Int = list.size
) : Browsable {
private val safeStart = start.coerceIn(0, list.size)
private val safeEnd = endExclusive.coerceIn(safeStart, list.size)
private val count = safeEnd - safeStart
override fun heading(): String {
val base = title ?: "List"
return if (safeStart == 0 && safeEnd == list.size) "$base (${list.size})" else "$base ${rangeLabel(safeStart, safeEnd)}"
}
override fun dir(): Map<String, Any?>? {
if (count <= 0) return null
if (count <= H2_LIST_LEAF_SIZE) {
val out = LinkedHashMap<String, Any?>(count)
for (i in safeStart until safeEnd) {
out["[$i]"] = wrapForInspector(list[i], title = "[$i]")
}
return out
}
val childSpan = computeChildSpan(count, H2_LIST_LEAF_SIZE)
val out = LinkedHashMap<String, Any?>()
var s = safeStart
while (s < safeEnd) {
val e = minOf(s + childSpan, safeEnd)
out[rangeLabel(s, e)] = PagedListBrowsable(list, title = title, start = s, endExclusive = e)
s = e
}
return out
}
override fun html(): String = "<i>${escapeHtml(heading())}</i>"
}
private data class MapEntryRecord(
val index: Int,
val key: Any?,
val value: Any?
)
private interface MapEntrySource {
val size: Int
fun slice(start: Int, endExclusive: Int): List<MapEntryRecord>
}
private class SnapshotMapEntrySource(map: Map<*, *>) : MapEntrySource {
private val entries: List<Pair<Any?, Any?>> = map.entries.map { it.key to it.value }
override val size: Int get() = entries.size
override fun slice(start: Int, endExclusive: Int): List<MapEntryRecord> {
val safeStart = start.coerceIn(0, size)
val safeEnd = endExclusive.coerceIn(safeStart, size)
if (safeStart >= safeEnd) return emptyList()
val out = ArrayList<MapEntryRecord>(safeEnd - safeStart)
for (i in safeStart until safeEnd) {
val (k, v) = entries[i]
out.add(MapEntryRecord(i, k, v))
}
return out
}
}
private class IteratingMapEntrySource(private val map: Map<*, *>) : MapEntrySource {
override val size: Int get() = map.size
override fun slice(start: Int, endExclusive: Int): List<MapEntryRecord> {
val safeStart = start.coerceIn(0, size)
val safeEnd = endExclusive.coerceIn(safeStart, size)
if (safeStart >= safeEnd) return emptyList()
val out = ArrayList<MapEntryRecord>(safeEnd - safeStart)
var index = 0
for ((k, v) in map) {
if (index >= safeEnd) break
if (index >= safeStart) out.add(MapEntryRecord(index, k, v))
index++
}
return out
}
}
private class MapEntryBrowsable(private val record: MapEntryRecord) : Browsable {
override fun heading(): String = "Entry [${record.index}]"
override fun dir(): Map<String, Any?> {
return linkedMapOf(
"key" to wrapForInspector(record.key, "key"),
"value" to wrapForInspector(record.value, "value")
)
}
override fun html(): String {
return "<div>Entry [${record.index}]</div>"
}
}
private class PagedMapBrowsable(
private val source: MapEntrySource,
private val title: String? = null,
private val start: Int = 0,
private val endExclusive: Int = source.size
) : Browsable {
companion object {
fun forMap(map: Map<*, *>, title: String? = null): PagedMapBrowsable {
val src = if (map.size <= H2_MAP_SNAPSHOT_THRESHOLD) SnapshotMapEntrySource(map) else IteratingMapEntrySource(map)
return PagedMapBrowsable(src, title = title, start = 0, endExclusive = src.size)
}
}
private val safeStart = start.coerceIn(0, source.size)
private val safeEnd = endExclusive.coerceIn(safeStart, source.size)
private val count = safeEnd - safeStart
override fun heading(): String {
val base = title ?: "Map"
return if (safeStart == 0 && safeEnd == source.size) "$base (${source.size})" else "$base ${rangeLabel(safeStart, safeEnd)}"
}
override fun dir(): Map<String, Any?>? {
if (count <= 0) return null
if (count <= H2_MAP_LEAF_SIZE) {
val out = LinkedHashMap<String, Any?>(count)
source.slice(safeStart, safeEnd).forEach { entry ->
out["[${entry.index}] ${keyPreview(entry.key)}"] = MapEntryBrowsable(entry)
}
return out
}
val childSpan = computeChildSpan(count, H2_MAP_LEAF_SIZE)
val out = LinkedHashMap<String, Any?>()
var s = safeStart
while (s < safeEnd) {
val e = minOf(s + childSpan, safeEnd)
out[rangeLabel(s, e)] = PagedMapBrowsable(source, title = title, start = s, endExclusive = e)
s = e
}
return out
}
override fun html(): String = "<i>${escapeHtml(heading())}</i>"
}
private class FloatBufferBrowsable(
buffer: FloatBuffer,
private val title: String? = null
) : Browsable {
private val readOnly: FloatBuffer = buffer.asReadOnlyBuffer()
private val position: Int = buffer.position()
private val limit: Int = buffer.limit()
private val capacity: Int = buffer.capacity()
override fun heading(): String = title ?: "FloatBuffer"
override fun dir(): Map<String, Any?> {
val out = LinkedHashMap<String, Any?>()
out["position"] = position
out["limit"] = limit
out["capacity"] = capacity
for (stride in 1..16) {
val rows = ceilDiv(limit, stride)
out["stride $stride ($rows rows)"] = FloatBufferRowsBrowsable(
buffer = readOnly,
stride = stride,
rowStart = 0,
rowEndExclusive = rows,
title = "stride $stride"
)
}
return out
}
override fun html(): String {
return "<div>FloatBuffer pos=$position limit=$limit capacity=$capacity</div>"
}
}
private class FloatBufferRowsBrowsable(
private val buffer: FloatBuffer,
private val stride: Int,
private val rowStart: Int,
private val rowEndExclusive: Int,
private val title: String
) : Browsable {
private val safeStride = stride.coerceAtLeast(1)
private val totalValues = buffer.limit()
private val totalRows = ceilDiv(totalValues, safeStride)
private val safeStart = rowStart.coerceIn(0, totalRows)
private val safeEnd = rowEndExclusive.coerceIn(safeStart, totalRows)
private val rowCount = safeEnd - safeStart
override fun heading(): String {
return "$title ${rangeLabel(safeStart, safeEnd)}"
}
override fun dir(): Map<String, Any?>? {
if (rowCount <= 0) return null
if (rowCount <= H2_FLOAT_ROWS_LEAF_SIZE) return null
val childSpan = computeChildSpan(rowCount, H2_FLOAT_ROWS_LEAF_SIZE)
val out = LinkedHashMap<String, Any?>()
var s = safeStart
while (s < safeEnd) {
val e = minOf(s + childSpan, safeEnd)
out[rangeLabel(s, e)] = FloatBufferRowsBrowsable(
buffer = buffer,
stride = safeStride,
rowStart = s,
rowEndExclusive = e,
title = title
)
s = e
}
return out
}
override fun html(): String {
if (rowCount <= 0) return "<i>empty</i>"
if (rowCount > H2_FLOAT_ROWS_LEAF_SIZE) {
return "<div>${escapeHtml(heading())}</div>"
}
return FieldMarkdown.card(
"""
{muted:rows $safeStart..${safeEnd - 1} | stride $safeStride}
${bufferPageTable(safeStart, safeEnd, safeStride, totalValues) { fmtFloat(buffer.get(it)) }}
"""
)
}
}
One page of a strided buffer as a table: a row index, then stride right-aligned cells drawn by
cell, with anything past total left blank.
The Float and Int browsers below had a copy of this each, identical but for the cell formatter.
maxRows is pinned to the page height because the pager has already done the bounding — Table's
own elision would silently cut a page short.
source — 126 lines, from line 891
private fun bufferPageTable(
start: Int,
end: Int,
stride: Int,
total: Int,
cell: (Int) -> String,
): String {
val columns = (0 until stride).map { it.toString() }
val page = Table(listOf("row") + columns)
.rows(
(start until end).map { row ->
listOf<Any?>(row) + columns.indices.map { c ->
val index = row * stride + c
if (index < total) cell(index) else null
}
}
)
.role("row", CellRole.ACCENT)
.align("row", Align.LEFT)
columns.forEach { page.align(it, Align.RIGHT) }
page.maxRows = (end - start).coerceAtLeast(1)
return page.toHTML()
}
private class IntBufferBrowsable(
buffer: IntBuffer,
private val title: String? = null
) : Browsable {
private val readOnly: IntBuffer = buffer.asReadOnlyBuffer()
private val position: Int = buffer.position()
private val limit: Int = buffer.limit()
private val capacity: Int = buffer.capacity()
override fun heading(): String = title ?: "IntBuffer"
override fun dir(): Map<String, Any?> {
val out = LinkedHashMap<String, Any?>()
out["position"] = position
out["limit"] = limit
out["capacity"] = capacity
for (stride in 1..16) {
val rows = ceilDiv(limit, stride)
out["stride $stride ($rows rows)"] = IntBufferRowsBrowsable(
buffer = readOnly,
stride = stride,
rowStart = 0,
rowEndExclusive = rows,
title = "stride $stride"
)
}
return out
}
override fun html(): String {
return "<div>IntBuffer pos=$position limit=$limit capacity=$capacity</div>"
}
}
private class IntBufferRowsBrowsable(
private val buffer: IntBuffer,
private val stride: Int,
private val rowStart: Int,
private val rowEndExclusive: Int,
private val title: String
) : Browsable {
private val safeStride = stride.coerceAtLeast(1)
private val totalValues = buffer.limit()
private val totalRows = ceilDiv(totalValues, safeStride)
private val safeStart = rowStart.coerceIn(0, totalRows)
private val safeEnd = rowEndExclusive.coerceIn(safeStart, totalRows)
private val rowCount = safeEnd - safeStart
override fun heading(): String {
return "$title ${rangeLabel(safeStart, safeEnd)}"
}
override fun dir(): Map<String, Any?>? {
if (rowCount <= 0) return null
if (rowCount <= H2_FLOAT_ROWS_LEAF_SIZE) return null
val childSpan = computeChildSpan(rowCount, H2_FLOAT_ROWS_LEAF_SIZE)
val out = LinkedHashMap<String, Any?>()
var s = safeStart
while (s < safeEnd) {
val e = minOf(s + childSpan, safeEnd)
out[rangeLabel(s, e)] = IntBufferRowsBrowsable(
buffer = buffer,
stride = safeStride,
rowStart = s,
rowEndExclusive = e,
title = title
)
s = e
}
return out
}
override fun html(): String {
if (rowCount <= 0) return "<i>empty</i>"
if (rowCount > H2_FLOAT_ROWS_LEAF_SIZE) {
return "<div>${escapeHtml(heading())}</div>"
}
return FieldMarkdown.card(
"""
{muted:rows $safeStart..${safeEnd - 1} | stride $safeStride}
${bufferPageTable(safeStart, safeEnd, safeStride, totalValues) { buffer.get(it).toString() }}
"""
)
}
}
private fun escapeHtml(text: String): String {
return text
.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'")
}
private fun escapeHtmlAttr(text: String): String = escapeHtml(text)
val Any?.h2: String
The box-facing entry point: someBigThing.h2 is the explorable rendering, where .h is the inline one.
Returns the panel as an HTML string, so it is a value a box can return or hand to display() like any other.
val Any?.h2: String
get() = LazyValueVisualizer.visualize(this)
inline fun <reified T : Any> registerH2Value(noinline adapter: (T) -> H2ValueSnapshot) {
Registers a .h2 adapter for T, so that a value of that type is summarised or unwrapped the way adapter says
rather than by the reflective default.
Registration is by exact class, but lookup walks the superclass chain and each class's whole interface closure, so
an adapter registered against an interface serves every implementation of it. H2DomainAdapters.kt holds the ones
Field registers for its own types.
inline fun <reified T : Any> registerH2Value(noinline adapter: (T) -> H2ValueSnapshot) {
H2ValueRegistry.register(T::class.java, adapter)
}
internal fun h2GetRawValue(token: String): Any? = H2TokenRetention.fetch(token)
internal fun h2AdaptedValue(value: Any): Any? = H2ValueRegistry.adapt(value)?.value ?: value
Test seam: the value a registered .h2 adapter substitutes for value, or value itself when
no adapter matches.
H2ValueRegistry is an object with an internal adapt, and the substitution it performs is the
whole content of a hand-written adapter — so without this, an adapter's shape (does an Fn
become an x/f(x) table?) could only be checked through the full HTML string, which is a test that
passes on a coincidence.