Files
cc-utils/src/lib/ccTUI/framework.md

15 KiB
Raw Blame History

ccTUI 框架重构指南

本文档旨在指导 ccTUI 框架的重构工作。框架将用于 Minecraft ComputerCraft (CC: Tweaked) 环境,其设计灵感来源于现代前端框架 SolidJS并采用声明式、组件化的编程模型。

核心理念

  • 声明式 UI: 像 SolidJS 或 React 一样,通过编写函数式组件来描述 UI 状态,而不是手动操作界面。
  • 响应式状态管理: UI 会根据状态State/Signal的变化自动更新开发者无需手动重绘。
  • 组件化: 将 UI 拆分为可复用的组件(函数),每个组件负责自身的状态和渲染。
  • Flexbox 布局: 借鉴 Web 上的 Flexbox 模型,提供一套声明式的、强大的布局工具,以替代传统的绝对坐标布局。

目标 API 预览

为了直观地展示目标 API这里将 SolidJS 的 "Simple Todos" 示例与我们期望的 ccTUI 实现进行对比。

SolidJS (Web) 版本:

// ... imports
const App = () => {
  const [newTitle, setTitle] = createSignal("");
  const [todos, setTodos] = createLocalStore<TodoItem[]>("todo list", []);
  // ... logic
  return (
    <>
      <h3>Simple Todos Example</h3>
      <form onSubmit={addTodo}>
        <input /* ...props */ />
        <button>+</button>
      </form>
      <For each={todos}>
        {(todo, i) => (
          <div>
            <input type="checkbox" /* ...props */ />
            <input type="text" /* ...props */ />
            <button /* ...props */>x</button>
          </div>
        )}
      </For>
    </>
  );
};
render(App, document.getElementById("app")!);

ccTUI (ComputerCraft) 目标版本:

// ... imports
type TodoItem = { title: string; done: boolean };

const App = () => {
  const [newTitle, setTitle] = createSignal("");
  const [todos, setTodos] = createStore<TodoItem[]>([]); // 使用简化版的 Store

  const addTodo = () => {
    batch(() => {
      setTodos(todos.length, { title: newTitle(), done: false });
      setTitle("");
    });
  };

  return div({ class: "flex flex-col" }, // 使用类似 TailwindCSS 的类名进行布局
    h3("Simple Todos Example"),
    form({ onSubmit: addTodo, class: "flex flex-row" },
      input({
        placeholder: "enter todo and click +",
        value: newTitle, // 直接传递 Signal
        onInput: setTitle, // 直接传递 Setter
      }),
      button("+")
    ),
    For({ each: todos },
      (todo, i) => div({ class: "flex flex-row items-center" },
        input({
          type: "checkbox",
          checked: () => todo.done, // 通过 accessor 获取
          onChange: (checked) => setTodos(i(), "done", checked),
        }),
        input({
          type: "text",
          value: () => todo.title,
          onChange: (newTitle) => setTodos(i(), "title", newTitle),
        }),
        button({ onClick: () => setTodos((t) => removeIndex(t, i())) }, "x")
      )
    )
  );
};

render(App);

注意:上述 ccTUI 代码是设计目标,具体实现(如 createStore, removeIndex)需要被创建。


1. 基础组件 API

组件是返回 UIObject 的函数。第一个参数是 props 对象,后续参数是子组件。

容器与文本

  • div(props: DivProps, ...children: UIObject[]): UIObject

    • 通用容器组件,用于包裹其他组件并应用布局样式。
    • DivProps: { class?: string } - class 属性用于指定布局,详见“布局系统”。
  • label(props: LabelProps, text: string | Signal<string>): UIObject

    • 静态或动态文本标签。
    • LabelProps: { class?: string }
  • h1, h2, h3(text): UIObject

    • 预设样式的标题标签,本质是 label 的封装。

交互组件

  • button(props: ButtonProps, text: string): UIObject

    • 可点击的按钮。
    • ButtonProps: { onClick?: () => void, class?: string }
    • 按钮会在被点击时调用 onClick 回调。
  • input(props: InputProps): UIObject

    • 文本或复选框输入。
    • InputProps:
      • type?: "text" | "checkbox" (默认为 "text")
      • value?: Signal<string>: (用于 text) 文本内容的 Signal。
      • onInput?: (value: string) => void: (用于 text) 内容变化时的回调。
      • checked?: Signal<boolean>: (用于 checkbox) 选中状态的 Signal。
      • onChange?: (checked: boolean) => void: (用于 checkbox) 状态变化时的回调。
      • placeholder?: string
      • class?: string
  • form(props: FormProps, ...children: UIObject[]): UIObject

    • 表单容器,主要用于组织输入组件。
    • FormProps: { onSubmit?: () => void, class?: string }
    • 在表单内按回车键(或点击提交按钮,如果未来实现)会触发 onSubmit

2. 控制流

  • For<T>(props: ForProps<T>, renderFn: (item: T, index: number) => UIObject): UIObject

    • 用于渲染列表。它会根据 each 数组的变化,高效地创建、销毁或更新子组件。
    • ForProps: { each: Signal<T[]> }
    • renderFn: 一个函数,接收当前项和索引,返回用于渲染该项的 UIObject
  • Show(props: ShowProps, child: UIObject): UIObject

    • 用于条件渲染。当 when 条件为 true 时渲染 child,否则渲染 fallback
    • ShowProps:
      • when: () => boolean: 一个返回布尔值的访问器函数 (accessor)。
      • fallback?: UIObject: 当 when 返回 false 时要渲染的组件。
    • child: 当 when 返回 true 时要渲染的组件。

    SolidJS 示例:

    import { createSignal, Show } from "solid-js";
    
    function App() {
      const [loggedIn, setLoggedIn] = createSignal(false);
      const toggle = () => setLoggedIn(!loggedIn());
    
      return (
        <Show
          when={loggedIn()}
          fallback={<button onClick={toggle}>Log In</button>}
        >
          <button onClick={toggle}>Log Out</button>
        </Show>
      );
    }
    

    ccTUI 目标版本:

    const App = () => {
      const [loggedIn, setLoggedIn] = createSignal(false);
      const toggle = () => setLoggedIn(!loggedIn());
    
      return Show(
        {
          when: loggedIn, // 直接传递 Signal 的 getter
          fallback: button({ onClick: toggle }, "Log In"),
        },
        button({ onClick: toggle }, "Log Out")
      );
    };
    

3. 布局系统 (Flexbox)

借鉴 TailwindCSS 的类名系统,通过 class 属性为 div 等容器组件提供布局指令。渲染引擎需要解析这些类名并应用 Flexbox 算法。

核心类名

  • flex: 必须。将容器声明为 Flex 容器。
  • flex-row: (默认) 主轴方向为水平。
  • flex-col: 主轴方向为垂直。

对齐与分布 (Justify & Align)

  • justify-start: (默认) 从主轴起点开始排列。

  • justify-center: 主轴居中。

  • justify-end: 从主轴终点开始排列。

  • justify-between: 两端对齐,项目之间的间隔都相等。

  • items-start: 交叉轴的起点对齐。

  • items-center: 交叉轴的中点对齐。

  • items-end: 交叉轴的终点对齐。

示例

// 一个垂直居中的登录框
div({ class: "flex flex-col justify-center items-center" },
  label("Username"),
  input({}),
  label("Password"),
  input({}),
  button("Login")
)

实现要点

渲染引擎在计算布局时:

  1. 解析 class 字符串,转换为布局属性(如 flexDirection, justifyContent)。
  2. 实现一个简化的 Flexbox 算法,该算法能根据容器尺寸、子元素尺寸和布局属性,为每个子元素计算出正确的 (x, y) 坐标和 (width, height)
  3. draw 阶段,将计算出的区域传递给子组件进行绘制。

4. 响应式系统 (Reactivity System)

框架的核心是其细粒度的响应式系统。该系统由 Signal 和 Effect 组成,其设计深受 SolidJS 启发。理解这两者是构建动态UI的关键。

createSignal: 响应式的基本单元

Signal 是一个包含值的“盒子”,当它的值发生变化时,它可以通知所有正在“监听”它的代码。

  • createSignal<T>(initialValue: T): [() => T, (newValue: T) => void]

    • 它接收一个初始值,并返回一个包含两个函数的数组:一个 getter 和一个 setter
    • Getter (() => T): 一个无参数的函数,调用它会返回 Signal 的当前值。重要的是,在特定上下文(如组件渲染或 Effect 中)调用 getter 会自动将该上下文注册为监听者。
    • Setter ((newValue: T) => void): 一个函数,用于更新 Signal 的值。调用它会触发所有监听该 Signal 的上下文重新执行。

    示例:

    // 1. 创建一个 signal
    const [count, setCount] = createSignal(0);
    
    // 2. 读取值 (这是一个函数调用)
    print(count()); // 输出: 0
    
    // 3. 更新值
    setCount(1);
    print(count()); // 输出: 1
    
    // 4. 在组件中使用 (当 count 变化时label 会自动更新)
    label({}, () => `Count: ${count()}`);
    

createEffect: 响应 Signal 的变化

Effect 用于将响应式系统与外部世界如日志、计时器、手动API调用连接起来。它是一个自动跟踪其依赖即它内部读取的 Signal并重新执行的函数。

  • createEffect(fn: () => void): void

    • 它接收一个函数 fn 并立即执行一次。
    • 框架会监视 fn 在执行期间读取了哪些 Signal (调用了哪些 getter)。
    • 当任何一个被依赖的 Signal 更新时,fn 会被自动重新执行。

    示例:

    const [count, setCount] = createSignal(0);
    
    // 创建一个 effect 来响应 count 的变化
    createEffect(() => {
      // 这个 effect 读取了 count(),因此它依赖于 count Signal
      print(`The current count is: ${count()}`);
    });
    // 控制台立即输出: "The current count is: 0"
    
    // 稍后在代码的其他地方更新 signal
    setCount(5);
    // effect 会自动重新运行,控制台输出: "The current count is: 5"
    

更新与批处理

  • batch(fn: () => void)

    • 将多次状态更新合并为一次,以进行单次、高效的 UI 重绘。如果你需要在一个操作中连续多次调用 setter,应该将它们包裹在 batch 中以获得最佳性能。
    batch(() => {
      setFirstName("John");
      setLastName("Smith");
    }); // UI 只会更新一次
    

复杂状态管理

  • createStore<T extends object>(initialValue: T): [T, (updater: ...) => void]
    • 用于响应式地管理对象和数组。与 createSignal 管理单个值不同,createStore 允许你独立地更新对象或数组的特定部分,并只触发关心这些部分的更新。其 API 应参考 SolidJS 的 createStore

5. 代码规范与构建

  • 代码规范:
    • 使用 unknown 代替 any
    • 使用 undefined 代替 null
    • 遵循 TSDoc 规范为所有函数、参数、返回值、分支和循环添加注释。
  • 构建与验证:
    • 使用 just build-example sync 命令构建示例代码并检查编译时错误。
    • 使用 pnpm dlx eslint [file] 命令对修改后的文件进行代码风格检查。

6. 文件结构说明

本节旨在说明 ccTUI 框架核心目录下的主要文件及其职责。

src/lib/ccTUI/

  • index.ts: 框架的公共 API 入口。所有可供外部使用的组件(如 div, button)和函数(如 createSignal)都应由此文件导出。
  • Signal.ts: 包含框架的响应式系统核心,即 createSignal, createEffect, batch 等的实现。
  • UIObject.ts: 所有 UI 元素的基类或基础类型。定义了如位置、尺寸、父子关系、绘制draw和更新update等通用接口。
  • TUIApplication.ts: 应用程序的根实例。负责管理主窗口、事件循环event loop、焦点管理和全局重绘。
  • UIWindow.ts: 代表一个独立的窗口(通常是整个终端屏幕),作为所有 UI 元素的根容器和绘制表面。
  • TextLabel.ts, Button.ts, InputField.ts: 具体的基础组件实现。
  • framework.md: (本文档) 框架的设计指南、API 参考和代码规范。

7. 框架示例

  • src/tuiExample/main.ts
    • 此文件是 ccTUI 框架的功能示例和测试场(使用旧的 API
  • src/tuiExample/main.new.ts
    • 新的响应式框架示例,展示 SolidJS 风格的 API。
    • 在对框架进行任何修改或添加新功能后,都应在此文件中创建相应的示例来验证其正确性并进行展示。
    • 使用 just build-example sync 命令可以编译此示例并将其同步到游戏内的 computer 目录中,以便在 Minecraft 环境中实际运行和查看效果。

8. 实现状态

已实现的功能

响应式系统 (reactivity.ts)

  • createSignal<T>(initialValue: T) - 创建响应式信号
  • createEffect(fn: () => void) - 创建自动跟踪依赖的副作用
  • batch(fn: () => void) - 批量更新多个信号
  • createMemo<T>(fn: () => T) - 创建派生信号(计算属性)

Store (store.ts)

  • createStore<T>(initialValue: T) - 创建响应式存储,用于管理对象和数组
  • removeIndex<T>(array: T[], index: number) - 辅助函数:从数组中移除元素
  • insertAt<T>(array: T[], index: number, item: T) - 辅助函数:插入元素到数组

基础组件 (components.ts)

  • div(props, ...children) - 通用容器组件
  • label(props, text) - 文本标签组件
  • h1(text), h2(text), h3(text) - 标题组件
  • button(props, text) - 按钮组件
  • input(props) - 输入组件(支持 text 和 checkbox 类型)
  • form(props, ...children) - 表单容器组件

控制流 (controlFlow.ts)

  • For<T>(props, renderFn) - 列表渲染组件
  • Show(props, child) - 条件渲染组件

布局系统 (layout.ts)

  • Flexbox 布局引擎实现
  • 支持的类名:
    • flex-row, flex-col - 设置 flex 方向
    • justify-start, justify-center, justify-end, justify-between - 主轴对齐
    • items-start, items-center, items-end - 交叉轴对齐

渲染器 (renderer.ts)

  • 将 UI 树渲染到 ComputerCraft 终端
  • 支持响应式文本内容
  • 处理焦点状态的视觉反馈

应用程序 (application.ts)

  • Application 类 - 管理应用生命周期
  • render(rootFn) - 便捷的渲染函数
  • 事件循环(键盘、鼠标)
  • 自动焦点管理
  • 响应式重渲染

📋 API 导出 (index.ts)

  • 所有新 API 已正确导出
  • 保留旧 API 以实现向后兼容

🎯 示例代码

  • main.new.ts - 简单的计数器示例,演示响应式系统的基本用法

🔄 向后兼容性

  • 旧的类组件系统Signal, UIComponent, Button 等)仍然可用
  • 旧的示例代码 main.ts 不受影响

9. 使用指南

基本示例

import { createSignal, div, label, button, render } from "../lib/ccTUI";

const App = () => {
  const [count, setCount] = createSignal(0);
  
  return div({ class: "flex flex-col" },
    label({}, () => `Count: ${count()}`),
    button({ onClick: () => setCount(count() + 1) }, "Increment")
  );
};

render(App);

构建与运行

# 构建示例
just build-example

# 构建并同步到游戏
just build-example sync

# 或使用 pnpm 直接构建
pnpm tstl -p ./tsconfig.tuiExample.json

代码检查

# 运行 ESLint 检查
pnpm dlx eslint src/lib/ccTUI/reactivity.ts