mirror of
https://github.com/SikongJueluo/cc-utils.git
synced 2025-11-05 03:37:50 +08:00
fix render bug and add todos demo
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user