Overview
Renderables are the fundamental building blocks of OpenTUI interfaces. Each Renderable is:
- A visual element (text, box, input field, etc.)
- Positioned using Yoga flexbox layout
- Part of a hierarchical tree structure
- Capable of handling mouse and keyboard events
- Independently styled and configurable
Creating Renderables
Renderables are created by instantiating their class with a renderer context:
import { TextRenderable, BoxRenderable } from "@opentui/core"
const text = new TextRenderable(renderer, {
id: "greeting",
content: "Hello, OpenTUI!",
fg: "#00FF00",
})
const box = new BoxRenderable(renderer, {
id: "container",
width: 30,
height: 10,
backgroundColor: "#333366",
})
Every renderable requires a RenderContext (the renderer) as its first argument.
Renderable Hierarchy
Parent-Child Relationships
Renderables form a tree structure using add() and remove():
const container = new GroupRenderable(renderer, {
id: "main-container",
})
const header = new TextRenderable(renderer, {
content: "Header",
})
const content = new BoxRenderable(renderer, {
height: 20,
})
// Add children
container.add(header)
container.add(content)
// Add to root to make visible
renderer.root.add(container)
Tree Navigation
// Get children
const children = container.getChildren()
const childCount = container.getChildrenCount()
// Find by ID (direct children only)
const child = container.getRenderable("header")
// Find in entire subtree
const descendant = container.findDescendantById("nested-element")
// Access parent
if (child.parent) {
console.log("Parent ID:", child.parent.id)
}
Insertion & Removal
// Insert before another child
container.insertBefore(newChild, existingChild)
// Insert at specific index
container.add(newChild, 0) // Insert at beginning
// Remove by ID
container.remove("child-id")
// Destroy (removes from parent and cleans up)
child.destroy()
// Destroy entire subtree
container.destroyRecursively()
Once destroyed, a renderable cannot be reused. Create a new instance instead.
Positioning & Sizing
Dimensions
Renderables support fixed, auto, and percentage-based sizing:
const box = new BoxRenderable(renderer, {
width: 50, // Fixed width: 50 cells
height: "auto", // Auto height based on content
minWidth: 20, // Minimum width constraint
maxWidth: 100, // Maximum width constraint
minHeight: "10%", // Percentage of parent
maxHeight: "50%",
})
// Access computed dimensions
console.log(box.width, box.height) // Actual rendered size
Position Types
Renderables can be positioned relatively (in flexbox flow) or absolutely:
// Relative positioning (default) - part of flexbox layout
const relativeBox = new BoxRenderable(renderer, {
position: "relative",
})
// Absolute positioning - removed from flow
const absoluteBox = new BoxRenderable(renderer, {
position: "absolute",
top: 5,
left: 10,
width: 20,
height: 8,
})
Edge Offsets
const positioned = new BoxRenderable(renderer, {
position: "absolute",
top: 0, // Distance from top
right: 0, // Distance from right
bottom: "auto", // Auto-calculated
left: 10, // Distance from left
})
// Update positions dynamically
positioned.top = 5
positioned.left = 15
Coordinates
Renderables track both local and absolute positions:
// Absolute screen coordinates
console.log(renderable.x, renderable.y)
// Translation offsets
renderable.translateX = 5
renderable.translateY = 3
Styling
Visibility
renderable.visible = false // Hide (removes from layout)
renderable.visible = true // Show
if (renderable.visible) {
console.log("Renderable is visible")
}
Opacity
renderable.opacity = 0.5 // 50% transparent (0.0 - 1.0)
Opacity affects the renderable and all its children.
Z-Index
Control rendering order (higher values render on top):
const background = new BoxRenderable(renderer, {
zIndex: 0,
})
const foreground = new BoxRenderable(renderer, {
zIndex: 10,
})
// Update dynamically
foreground.zIndex = 20
Overflow
Control how content beyond bounds is handled:
const scrollable = new BoxRenderable(renderer, {
overflow: "scroll", // Enable scrolling
// or "hidden" - clip content
// or "visible" - show all (default)
})
Rendering
Custom Rendering
Renderables can define custom rendering logic:
class CustomRenderable extends Renderable {
protected renderSelf(buffer: OptimizedBuffer, deltaTime: number): void {
// Draw custom content
buffer.drawText(
"Custom content",
this.x,
this.y,
RGBA.fromHex("#FFFFFF")
)
}
protected onUpdate(deltaTime: number): void {
// Called before rendering each frame
// Update internal state here
}
}
Render Hooks
Add logic before or after rendering:
const box = new BoxRenderable(renderer, {
renderBefore(buffer, deltaTime) {
// Runs before this renderable renders
console.log("About to render")
},
renderAfter(buffer, deltaTime) {
// Runs after this renderable renders
console.log("Finished rendering")
},
})
Buffered Rendering
Enable double-buffering for complex renderables:
const buffered = new BoxRenderable(renderer, {
buffered: true, // Render to offscreen buffer first
})
// Access the frame buffer
if (buffered.frameBuffer) {
buffered.frameBuffer.drawText(
"Buffered text",
0, 0,
RGBA.fromHex("#FFFFFF")
)
}
Buffering is useful for renderables with expensive drawing operations that don’t change every frame.
Live Renderables
Mark renderables that need continuous updates:
const animated = new BoxRenderable(renderer, {
live: true, // Request continuous rendering
})
// Dynamically toggle
animated.live = false // Stop continuous updates
When a renderable is live: true, it keeps the render loop active even when nothing changes.
Events
Mouse Events
import { MouseEvent } from "@opentui/core"
const interactive = new BoxRenderable(renderer, {
onMouseDown(event: MouseEvent) {
console.log(`Mouse down at (${event.x}, ${event.y})`)
console.log("Button:", event.button)
// Prevent default behavior
event.preventDefault()
// Stop propagation to parent
event.stopPropagation()
},
onMouseUp(event: MouseEvent) {
console.log("Mouse released")
},
onMouseMove(event: MouseEvent) {
// Track mouse movement
},
onMouseDrag(event: MouseEvent) {
// Track dragging
},
onMouseScroll(event: MouseEvent) {
if (event.scroll?.direction === "up") {
console.log("Scrolled up")
}
},
onMouseOver(event: MouseEvent) {
console.log("Mouse entered")
},
onMouseOut(event: MouseEvent) {
console.log("Mouse left")
},
})
Focus Events
Focusable renderables receive keyboard input:
const input = new InputRenderable(renderer, {
focusable: true,
onKeyDown(key: KeyEvent) {
console.log("Key pressed:", key.name)
if (key.name === "enter") {
// Handle enter key
key.preventDefault()
}
},
})
// Listen for focus changes
input.on("focused", () => {
console.log("Input gained focus")
})
input.on("blurred", () => {
console.log("Input lost focus")
})
// Programmatic focus
input.focus()
input.blur()
// Check focus state
if (input.focused) {
console.log("Input is focused")
}
Layout Events
renderable.on("resize", () => {
console.log(`Resized to ${renderable.width}x${renderable.height}`)
})
renderable.on("layout-changed", () => {
console.log("Layout recalculated")
})
Size Change Callback
const box = new BoxRenderable(renderer, {
onSizeChange() {
console.log("Size changed!")
},
})
Lifecycle
Update Cycle
Each frame, renderables go through:
- onUpdate(deltaTime) - Update state
- updateFromLayout() - Apply layout calculations
- renderSelf(buffer, deltaTime) - Draw to buffer
Lifecycle Pass
Register a renderable for lifecycle callbacks:
class MyRenderable extends Renderable {
constructor(ctx: RenderContext, options: RenderableOptions) {
super(ctx, options)
// Register for lifecycle pass
this.onLifecyclePass = () => {
// Called once per frame before layout
this.updateState()
}
}
}
Cleanup
class MyRenderable extends Renderable {
private timer: Timer
constructor(ctx: RenderContext, options: RenderableOptions) {
super(ctx, options)
this.timer = setInterval(() => {}, 1000)
}
protected destroySelf(): void {
// Clean up resources
clearInterval(this.timer)
}
protected onRemove(): void {
// Called when removed from parent (before destruction)
console.log("Removed from parent")
}
}
Selection
Renderables can support text selection:
const selectable = new TextRenderable(renderer, {
content: "Selectable text",
selectable: true,
})
// Check for selection
if (selectable.hasSelection()) {
const text = selectable.getSelectedText()
console.log("Selected:", text)
}
Properties Reference
Core Properties
renderable.id: string // Unique identifier
renderable.num: number // Internal numeric ID
renderable.ctx: RenderContext // Renderer context
renderable.parent: Renderable | null // Parent renderable
renderable.isDestroyed: boolean // Destruction state
Dimensions & Position
renderable.x: number // Absolute X coordinate
renderable.y: number // Absolute Y coordinate
renderable.width: number // Computed width
renderable.height: number // Computed height
renderable.translateX: number // Translation offset X
renderable.translateY: number // Translation offset Y
renderable.top: number | "auto" | `${number}%`
renderable.right: number | "auto" | `${number}%`
renderable.bottom: number | "auto" | `${number}%`
renderable.left: number | "auto" | `${number}%`
Style Properties
renderable.visible: boolean // Visibility
renderable.opacity: number // Opacity (0.0 - 1.0)
renderable.zIndex: number // Rendering order
renderable.overflow: "visible" | "hidden" | "scroll"
renderable.position: "relative" | "absolute"
State Properties
renderable.focusable: boolean // Can receive focus
renderable.focused: boolean // Currently focused
renderable.selectable: boolean // Supports selection
renderable.live: boolean // Continuous updates
renderable.liveCount: number // Live descendant count
renderable.isDirty: boolean // Needs re-render
Methods Reference
Tree Management
add(obj: Renderable, index?: number): number
insertBefore(obj: Renderable, anchor: Renderable): number
remove(id: string): void
getChildren(): Renderable[]
getChildrenCount(): number
getRenderable(id: string): Renderable | undefined
findDescendantById(id: string): Renderable | undefined
Rendering
requestRender(): void
render(buffer: OptimizedBuffer, deltaTime: number): void
markDirty(): void
Focus
focus(): void
blur(): void
Lifecycle
destroy(): void
destroyRecursively(): void