Renderables
Renderable 是 UI 的构建块。你可以定位、设置样式并将它们相互嵌套。每个 Renderable 代表一个视觉元素,使用 Yoga 布局引擎进行灵活的定位和大小调整。
创建 Renderable
通过使用渲染上下文(渲染器)和选项实例化类来创建 Renderable:
import { createCliRenderer, TextRenderable, BoxRenderable } from "@opentui/core"
const renderer = await createCliRenderer()
const greeting = new TextRenderable(renderer, {
id: "greeting",
content: "Hello, OpenTUI!",
fg: "#00FF00",
})
renderer.root.add(greeting)
可用的 Renderable
OpenTUI 提供以下内置 Renderable:
| 类 | 说明 |
|---|---|
BoxRenderable | 带边框、背景和布局的容器 |
TextRenderable | 只读的带样式文本显示 |
InputRenderable | 单行文本输入 |
TextareaRenderable | 多行可编辑文本 |
SelectRenderable | 下拉/列表选择 |
TabSelectRenderable | 水平标签选择 |
ScrollBoxRenderable | 可滚动容器 |
ScrollBarRenderable | 独立的滚动条控件 |
CodeRenderable | 语法高亮代码显示 |
LineNumberRenderable | 代码/文本视图的行号边栏 |
DiffRenderable | 统一或分栏差异查看器 |
ASCIIFontRenderable | ASCII 艺术字体显示 |
FrameBufferRenderable | 用于自定义图形的原始帧缓冲区 |
MarkdownRenderable | Markdown 渲染器 |
SliderRenderable | 数值滑块控件 |
二维码支持来自独立的 @opentui/qrcode 包。
Renderable 树
Renderable 形成树形结构。使用 add() 和 remove() 管理子元素:
const container = new BoxRenderable(renderer, {
id: "container",
flexDirection: "column",
padding: 1,
})
const title = new TextRenderable(renderer, { id: "title", content: "My App" })
const body = new TextRenderable(renderer, { id: "body", content: "Content here" })
container.add(title)
container.add(body)
renderer.root.add(container)
// 稍后,移除子元素
container.remove("body")
查找 Renderable
遍历树以查找特定的 Renderable:
// 通过 ID 获取直接子元素
const title = container.getRenderable("title")
// 递归搜索所有后代
const deepChild = container.findDescendantById("nested-input")
// 获取所有子元素
const children = container.getChildren()
布局属性
所有 Renderable 支持 Yoga flexbox 属性:
const panel = new BoxRenderable(renderer, {
id: "panel",
// 大小
width: 40,
height: "50%",
minWidth: 20,
maxHeight: 30,
// Flex 行为
flexGrow: 1,
flexShrink: 0,
flexDirection: "column",
justifyContent: "center",
alignItems: "flex-start",
// 定位
position: "absolute",
left: 10,
top: 5,
// 间距
padding: 2,
paddingTop: 1,
margin: 1,
})
详见布局页面。
焦点管理
交互式 Renderable 可以接收键盘焦点:
const input = new InputRenderable(renderer, {
id: "username",
placeholder: "Enter username...",
})
renderer.root.add(input)
// 给 input 焦点
input.focus()
// 移除焦点
input.blur()
// 检查焦点状态
console.log(input.focused) // true
默认情况下,左键单击 Renderable 会自动聚焦最近的可聚焦祖先。使用 createCliRenderer({ autoFocus: false }) 全局禁用此功能,或通过在 onMouseDown 中调用 event.preventDefault() 来逐次阻止。
监听焦点变化:
import { RenderableEvents } from "@opentui/core"
input.on(RenderableEvents.FOCUSED, () => {
console.log("Input focused")
})
input.on(RenderableEvents.BLURRED, () => {
console.log("Input blurred")
})
聚焦的后代
Renderable 还可以通过 hasFocusedDescendant 标志响应其后代中的焦点。当任何子元素获得焦点时,每个祖先的 hasFocusedDescendant 会切换为 true,OpenTUI 会标记它们需要重绘。BoxRenderable 使用此特性在框本身可聚焦且任何后代当前拥有焦点时绘制 focusedBorderColor:
const panel = new BoxRenderable(renderer, {
id: "panel",
focusable: true,
borderColor: "#444",
focusedBorderColor: "#00AAFF",
flexDirection: "column",
})
const input = new InputRenderable(renderer, { id: "panel-input" })
panel.add(input)
input.focus() // panel.hasFocusedDescendant === true → 边框变色
自定义 Renderable 可以读取 this._hasFocusedDescendant(受保护)或 renderable.hasFocusedDescendant(公共)来添加类似效果。
事件处理
鼠标事件
通过选项处理鼠标交互:
const button = new BoxRenderable(renderer, {
id: "button",
border: true,
onMouseDown: (event) => {
console.log("Clicked at", event.x, event.y)
},
onMouseOver: (event) => {
button.borderColor = "#FFFF00"
},
onMouseOut: (event) => {
button.borderColor = "#FFFFFF"
},
})
可用的鼠标事件:
onMouseDown、onMouseUponMouseMove、onMouseDrag、onMouseDragEnd、onMouseDroponMouseOver、onMouseOutonMouseScrollonMouse(全捕获)
鼠标事件会在树中冒泡。使用 event.stopPropagation() 停止传播。
键盘事件
对于可聚焦的 Renderable:
const textDecoder = new TextDecoder()
const input = new InputRenderable(renderer, {
id: "input",
onKeyDown: (key) => {
if (key.name === "escape") {
input.blur()
}
},
onPaste: (event) => {
console.log("Pasted:", textDecoder.decode(event.bytes))
},
})
可见性
使用 visible 属性控制可见性:
// 隐藏(同时从布局中移除)
panel.visible = false
// 显示
panel.visible = true
当 visible 为 false 时,Yoga 会将 Renderable 从布局计算中排除(相当于 CSS 的 display: none)。
不透明度
设置半透明渲染的不透明度:
panel.opacity = 0.5 // 50% 透明
不透明度影响 Renderable 及其所有子元素。
Z-Index
控制重叠元素的层叠顺序:
const overlay = new BoxRenderable(renderer, {
id: "overlay",
position: "absolute",
zIndex: 100, // 较高的值渲染在上面
})
实时渲染
对于动画,扩展 Renderable 类并重写 onUpdate:
class AnimatedBox extends BoxRenderable {
onUpdate(deltaTime) {
// 更新动画状态
this.translateX += 1
}
}
const box = new AnimatedBox(renderer, {
id: "anim-box",
live: true, // 启用连续渲染
})
平移
从布局位置偏移 Renderable(用于滚动/动画):
// 按像素偏移
renderable.translateX = 10
renderable.translateY = -5
这会在不影响布局的情况下视觉移动 Renderable。
缓冲渲染
为复杂内容启用离屏渲染,并使用钩子绘制到缓冲区:
import { RGBA } from "@opentui/core"
const complex = new BoxRenderable(renderer, {
id: "complex",
buffered: true, // 先渲染到离屏缓冲区
renderAfter: (buffer) => {
// 直接绘制到缓冲区(如果 buffered=true 则是离屏缓冲区)
buffer.fillRect(0, 0, 10, 5, RGBA.fromHex("#FF0000"))
},
})
renderBefore 和 renderAfter 是用于自定义渲染和装饰的纯绘制钩子。它们在布局和视口裁剪之后运行,因此不要从它们中修改布局属性、子元素、可见性或响应式状态。
生命周期方法
在自定义 Renderable 中重写这些方法:
class CustomRenderable extends Renderable {
// 每帧渲染前调用
onUpdate(deltaTime: number) {
// 更新状态、动画等
}
// 尺寸变化时调用
onResize(width: number, height: number) {
// 响应尺寸变化
}
// 从父元素移除时调用
onRemove() {
// 清理
}
// 重写以进行自定义渲染
renderSelf(buffer: OptimizedBuffer, deltaTime: number) {
// 绘制到缓冲区
}
}
销毁 Renderable
清理 Renderable 并从树中移除:
// 从父元素移除并释放资源
renderable.destroy()
// 销毁自身和所有子元素
container.destroyRecursively()