fix render bug and add todos demo

This commit is contained in:
2025-10-12 12:09:34 +08:00
parent 39598fe3e6
commit e4731a2cef
4 changed files with 190 additions and 37 deletions

View File

@@ -5,7 +5,7 @@
import { UIObject } from "./UIObject";
import { calculateLayout } from "./layout";
import { render as renderTree, clearScreen } from "./renderer";
import { createEffect } from "./reactivity";
import { CCLog } from "../ccLog";
/**
* Main application class
@@ -18,11 +18,14 @@ export class Application {
private focusedNode?: UIObject;
private termWidth: number;
private termHeight: number;
private logger: CCLog;
constructor() {
const [width, height] = term.getSize();
this.termWidth = width;
this.termHeight = height;
this.logger = new CCLog("tui_debug.log", false);
this.logger.debug("Application constructed.");
}
/**
@@ -63,6 +66,7 @@ export class Application {
clearScreen();
// Initial render
this.logger.debug("Initial renderFrame call.");
this.renderFrame();
// Main event loop
@@ -76,12 +80,14 @@ export class Application {
* Stop the application
*/
stop(): void {
this.logger.debug("Application stopping.");
this.running = false;
if (this.root !== undefined) {
this.root.unmount();
}
this.logger.close();
clearScreen();
}
@@ -89,13 +95,14 @@ export class Application {
* Render loop - continuously renders when needed
*/
private renderLoop(): void {
// Set up reactive rendering - re-render whenever any signal changes
createEffect(() => {
this.renderFrame();
});
while (this.running) {
// Keep the loop alive
if (this.needsRender) {
this.logger.debug(
"renderLoop: needsRender is true, calling renderFrame.",
);
this.needsRender = false;
this.renderFrame();
}
os.sleep(0.05);
}
}
@@ -105,7 +112,7 @@ export class Application {
*/
private renderFrame(): void {
if (this.root === undefined) return;
this.logger.debug("renderFrame: Calculating layout.");
// Calculate layout
calculateLayout(this.root, this.termWidth, this.termHeight, 1, 1);
@@ -113,7 +120,9 @@ export class Application {
clearScreen();
// Render the tree
this.logger.debug("renderFrame: Rendering tree.");
renderTree(this.root, this.focusedNode);
this.logger.debug("renderFrame: Finished rendering tree.");
}
/**
@@ -121,15 +130,20 @@ export class Application {
*/
private eventLoop(): void {
while (this.running) {
const [eventType, ...eventData] = os.pullEvent() as LuaMultiReturn<
unknown[]
>;
const [eventType, ...eventData] = os.pullEvent();
if (eventType === "key") {
this.handleKeyEvent(eventData[0] as number);
} else if (eventType === "char") {
this.handleCharEvent(eventData[0] as string);
} else if (eventType === "mouse_click") {
this.logger.debug(
string.format(
"eventLoop: Mouse click detected at (%d, %d)",
eventData[1],
eventData[2],
),
);
this.handleMouseClick(
eventData[0] as number,
eventData[1] as number,
@@ -204,10 +218,14 @@ export class Application {
private handleMouseClick(button: number, x: number, y: number): void {
if (button !== 1 || this.root === undefined) return;
this.logger.debug("handleMouseClick: Finding node.");
// Find which element was clicked
const clicked = this.findNodeAt(this.root, x, y);
if (clicked !== undefined) {
this.logger.debug(
string.format("handleMouseClick: Found node of type %s.", clicked.type),
);
// Set focus
this.focusedNode = clicked;
@@ -215,7 +233,12 @@ export class Application {
if (clicked.type === "button") {
const onClick = clicked.handlers.onClick;
if (onClick) {
this.logger.debug(
"handleMouseClick: onClick handler found, executing.",
);
(onClick as () => void)();
this.logger.debug("handleMouseClick: onClick handler finished.");
this.needsRender = true;
}
} else if (clicked.type === "input") {
const type = clicked.props.type as string | undefined;
@@ -229,11 +252,12 @@ export class Application {
) {
const currentValue = (checkedProp as () => boolean)();
(onChangeProp as (v: boolean) => void)(!currentValue);
this.needsRender = true;
}
}
}
this.needsRender = true;
} else {
this.logger.debug("handleMouseClick: No node found at click position.");
}
}
@@ -256,9 +280,19 @@ export class Application {
// Check this node
if (node.layout !== undefined) {
const { x: nx, y: ny, width, height } = node.layout;
if (x >= nx && x < nx + width && y >= ny && y < ny + height) {
const hit = x >= nx && x < nx + width && y >= ny && y < ny + height;
if (hit) {
this.logger.debug(
string.format(
"findNodeAt: Hit test TRUE for %s at (%d, %d)",
node.type,
nx,
ny,
),
);
// Only return interactive elements
if (node.type === "button" || node.type === "input") {
this.logger.debug("findNodeAt: Node is interactive, returning.");
return node;
}
}

View File

@@ -143,7 +143,7 @@ render(App);
- **`Show(props: ShowProps, child: UIObject): UIObject`**
- 用于条件渲染。当 `when` 条件为 `true` 时渲染 `child`,否则渲染 `fallback`
- `ShowProps`:
- `ShowProps`:
- `when: () => boolean`: 一个返回布尔值的访问器函数 (accessor)。
- `fallback?: UIObject`: 当 `when` 返回 `false` 时要渲染的组件。
- `child`: 当 `when` 返回 `true` 时要渲染的组件。
@@ -318,22 +318,22 @@ Effect 用于将响应式系统与外部世界如日志、计时器、手动A
### `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`**: 具体的基础组件实现
- **`reactivity.ts`**: 包含框架的响应式系统核心,即 `createSignal`, `createEffect`, `batch`, `createMemo` 等的实现。
- **`store.ts`**: 包含 `createStore` 的实现,用于管理对象和数组等复杂状态
- **`UIObject.ts`**: 定义了所有 UI 元素的基类或基础类型 `UIObject`包括位置、尺寸、父子关系、绘制draw和更新update等通用接口
- **`application.ts`**: 包含 `Application` 类负责管理主窗口、事件循环event loop、焦点管理和全局重绘。`render` 函数也在这里
- **`renderer.ts`**: 负责将 `UIObject` 树解析并绘制到 ComputerCraft 终端屏幕上
- **`layout.ts`**: Flexbox 布局引擎的实现。解析 `class` 属性并计算组件的布局。
- **`components.ts`**: 包含所有基础 UI 组件的实现,如 `div`, `label`, `button`, `input`, `form` 等。
- **`controlFlow.ts`**: 包含控制流组件,如 `For` 和 `Show`,用于处理列表渲染和条件渲染。
- **`framework.md`**: (本文档) 框架的设计指南、API 参考和代码规范。
---
## 7. 框架示例
- **`src/tuiExample/main.ts`**
- 此文件是 `ccTUI` 框架的功能示例和测试场(使用旧的 API
- **`src/tuiExample/main.new.ts`**
- 新的响应式框架示例,展示 SolidJS 风格的 API。
- 在对框架进行任何修改或添加新功能后,都应在此文件中创建相应的示例来验证其正确性并进行展示。
- **`src/tuiExample/main.ts`**: 此文件将作为新的响应式框架示例,用于展示和验证所有 SolidJS 风格的 API。
- 在对框架进行任何修改或添加新功能后,都应在此文件中创建或更新相应的示例来验证其正确性
- 使用 `just build-example sync` 命令可以编译此示例并将其同步到游戏内的 `computer` 目录中,以便在 Minecraft 环境中实际运行和查看效果。
---
@@ -406,7 +406,7 @@ 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")
@@ -435,4 +435,3 @@ pnpm tstl -p ./tsconfig.tuiExample.json
# 运行 ESLint 检查
pnpm dlx eslint src/lib/ccTUI/reactivity.ts
```

View File

@@ -17,10 +17,20 @@ function measureNode(node: UIObject): { width: number; height: number } {
const getTextContent = (): string => {
if (node.textContent !== undefined) {
if (typeof node.textContent === "function") {
return node.textContent();
return (node.textContent)();
}
return node.textContent;
}
// For nodes with text children, get their content
if (node.children.length > 0 && node.children[0].textContent !== undefined) {
const child = node.children[0];
if (typeof child.textContent === "function") {
return (child.textContent)();
}
return child.textContent!;
}
return "";
};
@@ -63,6 +73,8 @@ function measureNode(node: UIObject): { width: number; height: number } {
}
const direction = node.layoutProps.flexDirection ?? "row";
const isFlex = node.type === "div" || node.type === "form";
const gap = isFlex ? 1 : 0;
if (direction === "row") {
// In row direction, width is sum of children, height is max
@@ -71,6 +83,9 @@ function measureNode(node: UIObject): { width: number; height: number } {
totalWidth += childSize.width;
totalHeight = math.max(totalHeight, childSize.height);
}
if (node.children.length > 1) {
totalWidth += gap * (node.children.length - 1);
}
} else {
// In column direction, height is sum of children, width is max
for (const child of node.children) {
@@ -78,6 +93,9 @@ function measureNode(node: UIObject): { width: number; height: number } {
totalWidth = math.max(totalWidth, childSize.width);
totalHeight += childSize.height;
}
if (node.children.length > 1) {
totalHeight += gap * (node.children.length - 1);
}
}
return { width: totalWidth, height: totalHeight };
@@ -120,6 +138,9 @@ export function calculateLayout(
const justify = node.layoutProps.justifyContent ?? "start";
const align = node.layoutProps.alignItems ?? "start";
const isFlex = node.type === "div" || node.type === "form";
const gap = isFlex ? 1 : 0;
// Measure all children
const childMeasurements = node.children.map((child: UIObject) => measureNode(child));
@@ -139,6 +160,11 @@ export function calculateLayout(
}
}
// Add gaps to total size
if (node.children.length > 1) {
totalMainAxisSize += gap * (node.children.length - 1);
}
// Calculate starting position based on justify-content
let mainAxisPos = 0;
let spacing = 0;
@@ -187,6 +213,9 @@ export function calculateLayout(
}
mainAxisPos += measure.width + spacing;
if (i < node.children.length - 1) {
mainAxisPos += gap;
}
} else {
// Main axis is vertical
childY = startY + math.floor(mainAxisPos);
@@ -201,6 +230,9 @@ export function calculateLayout(
}
mainAxisPos += measure.height + spacing;
if (i < node.children.length - 1) {
mainAxisPos += gap;
}
}
// Recursively calculate layout for child