This document provides detailed API reference for Summon's Visual Builder infrastructure. These APIs enable building drag-and-drop UI editors, visual component builders, and live preview systems.
- ComponentRegistry
- JsonBlock
- RenderLoop
- SelectionManager
- HistoryManager
- PropertyBridge
- EditModeManager
- CollisionDetector
Thread-safe singleton registry for mapping component type keys to their factory functions.
object ComponentRegistry {
fun register(key: String, factory: ComponentFactory)
fun get(key: String): ComponentFactory
fun isRegistered(key: String): Boolean
fun unregister(key: String): Boolean
fun clear()
fun size(): Int
fun keys(): Set<String>
}typealias ComponentFactory = (props: Map<String, Any>) -> @Composable () -> UnitRegisters a component factory with the given key. If a factory already exists for the key, it will be overwritten (useful for hot reload).
Parameters:
key- The unique identifier for the component typefactory- The factory function that creates composables from props
Example:
ComponentRegistry.register("text") { props ->
{ Text(text = props["text"] as? String ?: "") }
}
ComponentRegistry.register("button") { props ->
{ Button(
onClick = props["onClick"] as? (() -> Unit) ?: {},
label = props["label"] as? String ?: "Button"
) }
}Retrieves a component factory by key. Returns a FallbackComponent factory if not found.
Parameters:
key- The component type key to look up
Returns: The registered factory, or a FallbackComponent factory if not found
Checks if a component is registered under the given key.
Removes a component registration. Returns true if removed.
Clears all component registrations.
Data class for JSON-based component tree representation.
data class JsonBlock(
val type: String,
val props: Map<String, Any> = emptyMap(),
val children: List<JsonBlock> = emptyList()
)type- The component type key registered in the ComponentRegistryprops- The properties map to pass to the component factorychildren- Optional list of child blocks for container components
// Create a 3-level deep JSON tree
val tree = JsonBlock(
type = "Column",
props = mapOf("spacing" to 8),
children = listOf(
JsonBlock(
type = "Text",
props = mapOf("text" to "Hello"),
children = emptyList()
),
JsonBlock(
type = "Button",
props = mapOf("label" to "Click me"),
children = emptyList()
)
)
)Reactive render loop with batched updates and frame-rate limiting for efficient visual builder rendering.
object RenderLoop {
val isRunning: SummonMutableState<Boolean>
var targetFps: Int
fun start()
fun stop()
fun requestRender()
fun onFrame(callback: (deltaTime: Long) -> Unit)
fun removeFrameCallback(callback: (deltaTime: Long) -> Unit)
}Starts the render loop. Frame callbacks will be invoked at the target FPS.
Stops the render loop.
Requests a single render frame (batched with other requests).
Registers a callback to be invoked on each frame. deltaTime is milliseconds since last frame.
// Start render loop at 60 FPS
RenderLoop.targetFps = 60
RenderLoop.start()
// Register frame callback
RenderLoop.onFrame { deltaTime ->
// Update animations, render preview, etc.
updateAnimations(deltaTime)
}
// Request render when something changes
fun onPropertyChange() {
RenderLoop.requestRender()
}Singleton for managing component selection state in visual editors.
object SelectionManager {
val selectedId: SummonMutableState<String?>
var onSelectionChange: ((String?) -> Unit)?
fun select(componentId: String)
fun deselect()
fun toggle(componentId: String)
fun hasSelection(): Boolean
fun reset()
}selectedId- Reactive state holding the currently selected component ID (null if none)onSelectionChange- Optional callback invoked when selection changes
Selects a component by ID.
Clears the current selection.
Toggles selection for a component (selects if not selected, deselects if already selected).
Returns true if any component is currently selected.
Resets the selection manager to initial state.
// Select a component
SelectionManager.select("component-123")
// Listen for selection changes
SelectionManager.onSelectionChange = { id ->
if (id != null) {
showPropertyPanel(id)
} else {
hidePropertyPanel()
}
}
// Check selection state
if (SelectionManager.hasSelection()) {
val selected = SelectionManager.selectedId.value
// ...
}Generic undo/redo history manager with state snapshots.
class HistoryManager<T>(
var maxHistorySize: Int = 50
) {
fun push(state: T)
fun current(): T
fun canUndo(): Boolean
fun canRedo(): Boolean
fun undo(): Boolean
fun redo(): Boolean
fun size(): Int
fun clear()
}Pushes a new state onto the history stack. Discards any future states if not at the end.
Returns the current state. Throws NoSuchElementException if history is empty.
Checks if undo/redo is available.
Navigates through history. Returns true if the operation was performed.
Specialized singleton for JSON component tree history:
object JsonTreeHistoryManager {
val currentState: SummonMutableState<List<JsonBlock>>
val canUndo: SummonMutableState<Boolean>
val canRedo: SummonMutableState<Boolean>
var maxHistorySize: Int
fun initialize(initialState: List<JsonBlock>)
fun push(newState: List<JsonBlock>)
fun undo(): Boolean
fun redo(): Boolean
fun getCurrentState(): List<JsonBlock>
fun clear()
}// Generic history for any state type
val history = HistoryManager<MyState>()
history.push(initialState)
history.push(newState)
history.undo() // Back to initialState
history.redo() // Forward to newState
// JSON tree history (singleton)
JsonTreeHistoryManager.initialize(initialTree)
JsonTreeHistoryManager.push(updatedTree)
if (JsonTreeHistoryManager.canUndo.value) {
JsonTreeHistoryManager.undo()
}Bridge for live property updates between visual editor and renderer.
object PropertyBridge {
val currentTree: SummonMutableState<List<JsonBlock>>
var onPropertyChange: ((componentId: String, propName: String, value: Any?) -> Unit)?
fun registerComponent(componentId: String, initialProps: Map<String, Any?>)
fun updateProperty(componentId: String, propName: String, value: Any?): Boolean
fun updateProperties(componentId: String, properties: Map<String, Any?>): Boolean
fun getProperty(componentId: String, propName: String): Any?
fun getAllProperties(componentId: String): Map<String, Any?>
fun removeProperty(componentId: String, propName: String)
fun addChild(parentId: String, child: JsonBlock, index: Int = -1): Boolean
fun removeComponent(componentId: String): Boolean
fun moveComponent(componentId: String, newParentId: String, index: Int = -1): Boolean
fun clear()
fun syncFromHistory()
}Registers a component with its initial properties for tracking.
Updates a single property on a component. Returns true if successful.
Adds a new child to a container component. Use index = -1 to append at end.
Removes a component from the tree.
Moves a component to a new parent or position.
// Register component
PropertyBridge.registerComponent("text-1", mapOf("text" to "Hello"))
// Update property (triggers onPropertyChange)
PropertyBridge.updateProperty("text-1", "text", "World")
// Listen for changes
PropertyBridge.onPropertyChange = { id, prop, value ->
println("Component $id property '$prop' changed to: $value")
}
// Add child to container
val newButton = JsonBlock("Button", mapOf("label" to "New"))
PropertyBridge.addChild("container-1", newButton)Singleton for toggling between edit and preview modes.
object EditModeManager {
val isEditMode: SummonMutableState<Boolean>
var onModeChange: ((Boolean) -> Unit)?
fun enterEditMode()
fun exitEditMode()
fun toggleEditMode()
fun reset()
}isEditMode- Reactive state indicating current mode (true= edit,false= preview)onModeChange- Optional callback invoked when mode changes
// Toggle edit mode
Button(onClick = { EditModeManager.toggleEditMode() }) {
Text(if (EditModeManager.isEditMode.value) "Preview" else "Edit")
}
// Listen for mode changes
EditModeManager.onModeChange = { isEdit ->
if (isEdit) {
showEditToolbar()
} else {
hideEditToolbar()
}
}Singleton for hit testing and drop zone management in drag-and-drop interfaces.
object CollisionDetector {
fun registerDropZone(id: String, bounds: Bounds, priority: Int = 0)
fun unregisterDropZone(id: String)
fun updateDropZone(id: String, bounds: Bounds)
fun findDropTarget(x: Double, y: Double): DropZone?
fun clear()
}data class Bounds(
val x: Double,
val y: Double,
val width: Double,
val height: Double
)
data class DropZone(
val id: String,
val bounds: Bounds,
val priority: Int = 0
)Registers a drop zone with its bounding rectangle. Higher priority zones are matched first when overlapping.
Finds the highest priority drop zone containing the given point.
// Register drop zones
CollisionDetector.registerDropZone(
id = "container-1",
bounds = Bounds(0.0, 0.0, 400.0, 300.0),
priority = 1
)
// During drag
fun onDrag(x: Double, y: Double) {
val target = CollisionDetector.findDropTarget(x, y)
if (target != null) {
highlightDropZone(target.id)
}
}
// On drop
fun onDrop(x: Double, y: Double, draggedId: String) {
val target = CollisionDetector.findDropTarget(x, y)
if (target != null) {
PropertyBridge.moveComponent(draggedId, target.id)
}
}Visual error component displayed when a component type is not found in the registry.
@Composable
fun FallbackComponent(missingKey: String, props: Map<String, Any>)Renders a container with a red dashed border showing the missing component key and its props, making missing components immediately visible during development.
- Register components early: Register all component types before building UI trees
- Use unique IDs: Ensure all component IDs are unique within the tree
- Batch property updates: Use
updateProperties()for multiple changes - Save history checkpoints: Push to history after logical edit operations, not every keystroke
- Clear on unmount: Call
clear()methods when unmounting the builder - Handle missing components: The FallbackComponent makes debugging easier - check the console for warnings
- Core API - Core interfaces and classes
- State API - State management utilities
- Components API - UI components reference