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统一或分栏差异查看器
ASCIIFontRenderableASCII 艺术字体显示
FrameBufferRenderable用于自定义图形的原始帧缓冲区
MarkdownRenderableMarkdown 渲染器
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"
  },
})

可用的鼠标事件:

  • onMouseDownonMouseUp
  • onMouseMoveonMouseDragonMouseDragEndonMouseDrop
  • onMouseOveronMouseOut
  • onMouseScroll
  • onMouse(全捕获)

鼠标事件会在树中冒泡。使用 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

visiblefalse 时,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"))
  },
})

renderBeforerenderAfter 是用于自定义渲染和装饰的纯绘制钩子。它们在布局和视口裁剪之后运行,因此不要从它们中修改布局属性、子元素、可见性或响应式状态。

生命周期方法

在自定义 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()