add color style and polish input field

This commit is contained in:
2025-10-12 13:02:56 +08:00
parent e4731a2cef
commit 069196dfbb
7 changed files with 340 additions and 173 deletions

View File

@@ -19,3 +19,6 @@ build-example:
sync:
rsync --delete -r "./build/" "{{ sync-path }}"
lint:
pnpm dlx eslint src/**/*.ts --fix

View File

@@ -17,6 +17,16 @@ export interface LayoutProps {
alignItems?: "start" | "center" | "end";
}
/**
* Style properties for colors and appearance
*/
export interface StyleProps {
/** Text color */
textColor?: number;
/** Background color */
backgroundColor?: number;
}
/**
* Computed layout result after flexbox calculation
*/
@@ -74,6 +84,9 @@ export class UIObject {
/** Layout properties parsed from class string */
layoutProps: LayoutProps;
/** Style properties parsed from class string */
styleProps: StyleProps;
/** Whether this component is currently mounted */
mounted: boolean;
@@ -85,6 +98,9 @@ export class UIObject {
/** Event handlers */
handlers: Record<string, ((...args: unknown[]) => void) | undefined>;
/** For input text components - cursor position */
cursorPos?: number;
constructor(
type: UIObjectType,
@@ -95,21 +111,56 @@ export class UIObject {
this.props = props;
this.children = children;
this.layoutProps = {};
this.styleProps = {};
this.mounted = false;
this.cleanupFns = [];
this.handlers = {};
// Parse layout from class prop
this.parseLayout();
// Parse layout and styles from class prop
this.parseClassNames();
// Extract event handlers
this.extractHandlers();
// Initialize cursor position for text inputs
if (type === "input" && props.type !== "checkbox") {
this.cursorPos = 0;
}
}
/**
* Parse CSS-like class string into layout properties
* Map color name to ComputerCraft colors API value
*
* @param colorName - The color name from class (e.g., "white", "red")
* @returns The color value from colors API, or undefined if invalid
*/
private parseLayout(): void {
private parseColor(colorName: string): number | undefined {
const colorMap: Record<string, number> = {
"white": colors.white,
"orange": colors.orange,
"magenta": colors.magenta,
"lightBlue": colors.lightBlue,
"yellow": colors.yellow,
"lime": colors.lime,
"pink": colors.pink,
"gray": colors.gray,
"lightGray": colors.lightGray,
"cyan": colors.cyan,
"purple": colors.purple,
"blue": colors.blue,
"brown": colors.brown,
"green": colors.green,
"red": colors.red,
"black": colors.black,
};
return colorMap[colorName];
}
/**
* Parse CSS-like class string into layout and style properties
*/
private parseClassNames(): void {
const className = this.props.class as string | undefined;
if (className === undefined) return;
@@ -142,6 +193,24 @@ export class UIObject {
} else if (cls === "items-end") {
this.layoutProps.alignItems = "end";
}
// Text color (text-<color>)
else if (cls.startsWith("text-")) {
const colorName = cls.substring(5); // Remove "text-" prefix
const color = this.parseColor(colorName);
if (color !== undefined) {
this.styleProps.textColor = color;
}
}
// Background color (bg-<color>)
else if (cls.startsWith("bg-")) {
const colorName = cls.substring(3); // Remove "bg-" prefix
const color = this.parseColor(colorName);
if (color !== undefined) {
this.styleProps.backgroundColor = color;
}
}
}
// Set defaults

View File

@@ -19,6 +19,9 @@ export class Application {
private termWidth: number;
private termHeight: number;
private logger: CCLog;
private cursorBlinkState = false;
private lastBlinkTime = 0;
private readonly BLINK_INTERVAL = 0.5; // seconds
constructor() {
const [width, height] = term.getSize();
@@ -73,6 +76,7 @@ export class Application {
parallel.waitForAll(
() => this.renderLoop(),
() => this.eventLoop(),
() => this.timerLoop(),
);
}
@@ -121,10 +125,33 @@ export class Application {
// Render the tree
this.logger.debug("renderFrame: Rendering tree.");
renderTree(this.root, this.focusedNode);
renderTree(this.root, this.focusedNode, this.cursorBlinkState);
this.logger.debug("renderFrame: Finished rendering tree.");
}
/**
* Timer loop - handles cursor blinking
*/
private timerLoop(): void {
while (this.running) {
const currentTime = os.clock();
if (currentTime - this.lastBlinkTime >= this.BLINK_INTERVAL) {
this.lastBlinkTime = currentTime;
this.cursorBlinkState = !this.cursorBlinkState;
// Only trigger render if we have a focused text input
if (
this.focusedNode !== undefined &&
this.focusedNode.type === "input" &&
this.focusedNode.props.type !== "checkbox"
) {
this.needsRender = true;
}
}
os.sleep(0.05);
}
}
/**
* Event loop - handles user input
*/
@@ -186,6 +213,61 @@ export class Application {
}
}
}
} else if (this.focusedNode !== undefined && this.focusedNode.type === "input") {
// Handle text input key events
const type = this.focusedNode.props.type as string | undefined;
if (type !== "checkbox") {
this.handleTextInputKey(key);
}
}
}
/**
* Handle keyboard events for text input
*/
private handleTextInputKey(key: number): void {
if (this.focusedNode === undefined) return;
const valueProp = this.focusedNode.props.value;
const onInputProp = this.focusedNode.props.onInput;
if (
typeof valueProp !== "function" ||
typeof onInputProp !== "function"
) {
return;
}
const currentValue = (valueProp as () => string)();
const cursorPos = this.focusedNode.cursorPos ?? 0;
if (key === keys.left) {
// Move cursor left
this.focusedNode.cursorPos = math.max(0, cursorPos - 1);
this.needsRender = true;
} else if (key === keys.right) {
// Move cursor right
this.focusedNode.cursorPos = math.min(currentValue.length, cursorPos + 1);
this.needsRender = true;
} else if (key === keys.backspace) {
// Delete character before cursor
if (cursorPos > 0) {
const newValue =
currentValue.substring(0, cursorPos - 1) +
currentValue.substring(cursorPos);
(onInputProp as (v: string) => void)(newValue);
this.focusedNode.cursorPos = cursorPos - 1;
this.needsRender = true;
}
} else if (key === keys.delete) {
// Delete character after cursor
if (cursorPos < currentValue.length) {
const newValue =
currentValue.substring(0, cursorPos) +
currentValue.substring(cursorPos + 1);
(onInputProp as (v: string) => void)(newValue);
this.needsRender = true;
}
}
}
@@ -196,7 +278,7 @@ export class Application {
if (this.focusedNode !== undefined && this.focusedNode.type === "input") {
const type = this.focusedNode.props.type as string | undefined;
if (type !== "checkbox") {
// Add character to text input
// Insert character at cursor position
const onInputProp = this.focusedNode.props.onInput;
const valueProp = this.focusedNode.props.value;
@@ -205,7 +287,13 @@ export class Application {
typeof valueProp === "function"
) {
const currentValue = (valueProp as () => string)();
(onInputProp as (v: string) => void)(currentValue + char);
const cursorPos = this.focusedNode.cursorPos ?? 0;
const newValue =
currentValue.substring(0, cursorPos) +
char +
currentValue.substring(cursorPos);
(onInputProp as (v: string) => void)(newValue);
this.focusedNode.cursorPos = cursorPos + 1;
this.needsRender = true;
}
}
@@ -229,6 +317,15 @@ export class Application {
// Set focus
this.focusedNode = clicked;
// Initialize cursor position for text inputs on focus
if (clicked.type === "input" && clicked.props.type !== "checkbox") {
const valueProp = clicked.props.value;
if (typeof valueProp === "function") {
const currentValue = (valueProp as () => string)();
clicked.cursorPos = currentValue.length;
}
}
// Trigger click handler
if (clicked.type === "button") {
const onClick = clicked.handlers.onClick;
@@ -256,6 +353,8 @@ export class Application {
}
}
}
this.needsRender = true;
} else {
this.logger.debug("handleMouseClick: No node found at click position.");
}

View File

@@ -15,35 +15,6 @@
为了直观地展示目标 API这里将 SolidJS 的 "Simple Todos" 示例与我们期望的 ccTUI 实现进行对比。
**SolidJS (Web) 版本:**
```typescript
// ... 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) 目标版本:**
```typescript
// ... imports
@@ -96,27 +67,31 @@ render(App);
## 1. 基础组件 API
组件是返回 `UIObject` 的函数。第一个参数是 `props` 对象,后续参数是子组件
组件是返回 `UIObject` 的函数。所有组件的 `props` 对象都继承自一个基础接口,获得了通用能力
- **`MouseClickEvent`**: `{ button: number, x: number, y: number }`
- 描述一次鼠标点击事件的对象。
- **`BaseProps`**:
- `{ class?: string, onMouseClick?: (event: MouseClickEvent) => void }`
- 所有组件 `props` 都包含这两个可选属性,用于样式和通用的点击事件处理。
### 容器与文本
- **`div(props: DivProps, ...children: UIObject[]): UIObject`**
- 通用容器组件,用于包裹其他组件并应用布局样式。
- `DivProps`: `{ class?: string }` - `class` 属性用于指定布局,详见“布局系统”。
- **`div(props: BaseProps, ...children: UIObject[]): UIObject`**
- 通用容器组件,用于包裹其他组件并应用布局样式。
- **`label(props: LabelProps, text: string | Signal<string>): UIObject`**
- **`label(props: BaseProps, text: string | Signal<string>): UIObject`**
- 静态或动态文本标签。
- `LabelProps`: `{ class?: string }`
- **`h1`, `h2`, `h3`(text): UIObject**
- 预设样式的标题标签,本质是 `label` 的封装。
### 交互组件
- **`button(props: ButtonProps, text: string): UIObject`**
- **`button(props: { onClick?: () => void } & BaseProps, text: string): UIObject`**
- 可点击的按钮。
- `ButtonProps`: `{ onClick?: () => void, class?: string }`
- 按钮会在被点击时调用 `onClick` 回调。
- `onClick` 是一个为方便使用的回调,在鼠标左键点击时触发。
- **`input(props: InputProps): UIObject`**
- **`input(props: InputProps & BaseProps): UIObject`**
- 文本或复选框输入。
- `InputProps`:
- `type?: "text" | "checkbox"` (默认为 "text")
@@ -125,12 +100,25 @@ render(App);
- `checked?: Signal<boolean>`: (用于 checkbox) 选中状态的 Signal。
- `onChange?: (checked: boolean) => void`: (用于 checkbox) 状态变化时的回调。
- `placeholder?: string`
- `class?: string`
- **`form(props: FormProps, ...children: UIObject[]): UIObject`**
- **运行时行为 (Text Type)**:
- **焦点获取 (Focus)**: 当用户点击组件时,它将获得焦点。
- `placeholder` 文本会被清除。
- 在当前的输入位置会出现一个闪烁的光标(例如 `|`)。
- **焦点丢失 (Blur)**: 当用户点击其他组件时,它将失去焦点。
- 闪烁的光标消失。
- 如果此时输入框内容为空,`placeholder` 文本将重新显示。
- **光标移动**:
- `left_arrow` 键: 光标向左移动一个字符位置。
- `right_arrow` 键: 光标向右移动一个字符位置。
- **文本编辑**:
- `backspace` 键: 删除光标前的一个字符。
- `delete` 键: 删除光标后的一个字符。
- 其他字符按键: 在光标位置插入对应字符。
- **`form(props: { onSubmit?: () => void } & BaseProps, ...children: UIObject[]): UIObject`**
- 表单容器,主要用于组织输入组件。
- `FormProps`: `{ onSubmit?: () => void, class?: string }`
- 在表单内按回车键(或点击提交按钮,如果未来实现)会触发 `onSubmit`
- 在表单内按回车键会触发 `onSubmit`
---
@@ -148,41 +136,6 @@ render(App);
- `fallback?: UIObject`: 当 `when` 返回 `false` 时要渲染的组件。
- `child`: 当 `when` 返回 `true` 时要渲染的组件。
**SolidJS 示例:**
```typescript
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 目标版本:**
```typescript
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)
@@ -206,34 +159,24 @@ render(App);
- **`items-center`**: 交叉轴的中点对齐。
- **`items-end`**: 交叉轴的终点对齐。
### 示例
```typescript
// 一个垂直居中的登录框
div({ class: "flex flex-col justify-center items-center" },
label("Username"),
input({}),
label("Password"),
input({}),
button("Login")
)
```
### Color & Styling
### 实现要点
渲染引擎在计算布局时:
1. 解析 `class` 字符串,转换为布局属性(如 `flexDirection`, `justifyContent`)。
2. 实现一个简化的 Flexbox 算法,该算法能根据容器尺寸、子元素尺寸和布局属性,为每个子元素计算出正确的 `(x, y)` 坐标和 `(width, height)`。
3. 在 `draw` 阶段,将计算出的区域传递给子组件进行绘制。
除了布局,`class` 属性也用于控制组件的颜色。这借鉴了 TailwindCSS 的思想。
- **文本颜色**: `text-<color>`
- **背景颜色**: `bg-<color>`
#### 可用颜色
颜色名称直接映射自 `tweaked.cc``colors` API: `white`, `orange`, `magenta`, `lightBlue`, `yellow`, `lime`, `pink`, `gray`, `lightGray`, `cyan`, `purple`, `blue`, `brown`, `green`, `red`, `black`.
---
## 4. 响应式系统 (Reactivity System)
框架的核心是其细粒度的响应式系统。该系统由 Signal 和 Effect 组成,其设计深受 SolidJS 启发。理解这两者是构建动态UI的关键
框架的核心是其细粒度的响应式系统。该系统由 Signal 和 Effect 组成。
### `createSignal`: 响应式的基本单元
Signal 是一个包含值的“盒子”,当它的值发生变化时,它可以通知所有正在“监听”它的代码。
- **`createSignal<T>(initialValue: T): [() => T, (newValue: T) => void]`**
- 它接收一个初始值,并返回一个包含两个函数的数组:一个 `getter` 和一个 `setter`
- **Getter** (`() => T`): 一个无参数的函数,调用它会返回 Signal 的当前值。**重要的是,在特定上下文(如组件渲染或 Effect 中)调用 getter 会自动将该上下文注册为监听者。**
@@ -256,9 +199,6 @@ Signal 是一个包含值的“盒子”,当它的值发生变化时,它可
```
### `createEffect`: 响应 Signal 的变化
Effect 用于将响应式系统与外部世界如日志、计时器、手动API调用连接起来。它是一个自动跟踪其依赖即它内部读取的 Signal并重新执行的函数。
- **`createEffect(fn: () => void): void`**
- 它接收一个函数 `fn` 并立即执行一次。
- 框架会监视 `fn` 在执行期间读取了哪些 Signal (调用了哪些 getter)。
@@ -293,7 +233,6 @@ Effect 用于将响应式系统与外部世界如日志、计时器、手动A
```
### 复杂状态管理
- **`createStore<T extends object>(initialValue: T): [T, (updater: ...) => void]`**
- 用于响应式地管理对象和数组。与 `createSignal` 管理单个值不同,`createStore` 允许你独立地更新对象或数组的特定部分,并只触发关心这些部分的更新。其 API 应参考 SolidJS 的 `createStore`。
@@ -371,6 +310,8 @@ Effect 用于将响应式系统与外部世界如日志、计时器、手动A
- `flex-row`, `flex-col` - 设置 flex 方向
- `justify-start`, `justify-center`, `justify-end`, `justify-between` - 主轴对齐
- `items-start`, `items-center`, `items-end` - 交叉轴对齐
- `text-<color>` - 文本颜色(支持所有 CC 颜色)
- `bg-<color>` - 背景颜色(支持所有 CC 颜色)
#### 渲染器 (renderer.ts)
- ✅ 将 UI 树渲染到 ComputerCraft 终端
@@ -384,17 +325,6 @@ Effect 用于将响应式系统与外部世界如日志、计时器、手动A
- ✅ 自动焦点管理
- ✅ 响应式重渲染
### 📋 API 导出 (index.ts)
- ✅ 所有新 API 已正确导出
- ✅ 保留旧 API 以实现向后兼容
### 🎯 示例代码
- ✅ `main.new.ts` - 简单的计数器示例,演示响应式系统的基本用法
### 🔄 向后兼容性
- ✅ 旧的类组件系统Signal, UIComponent, Button 等)仍然可用
- ✅ 旧的示例代码 `main.ts` 不受影响
---
## 9. 使用指南
@@ -433,5 +363,8 @@ pnpm tstl -p ./tsconfig.tuiExample.json
```bash
# 运行 ESLint 检查
pnpm dlx eslint src/lib/ccTUI/reactivity.ts
pnpm dlx eslint src/**/*.ts
# OR
just lint
```

View File

@@ -50,6 +50,7 @@ export { Application, render } from "./application";
export {
UIObject,
type LayoutProps,
type StyleProps,
type ComputedLayout,
type BaseProps,
} from "./UIObject";

View File

@@ -33,8 +33,9 @@ function getTextContent(node: UIObject): string {
*
* @param node - The node to draw
* @param focused - Whether this node has focus
* @param cursorBlinkState - Whether the cursor should be visible (for blinking)
*/
function drawNode(node: UIObject, focused: boolean): void {
function drawNode(node: UIObject, focused: boolean, cursorBlinkState: boolean): void {
if (!node.layout) return;
const { x, y, width } = node.layout;
@@ -43,6 +44,10 @@ function drawNode(node: UIObject, focused: boolean): void {
const [origX, origY] = term.getCursorPos();
try {
// Default colors that can be overridden by styleProps
let textColor = node.styleProps.textColor;
const bgColor = node.styleProps.backgroundColor;
switch (node.type) {
case "label":
case "h1":
@@ -50,17 +55,21 @@ function drawNode(node: UIObject, focused: boolean): void {
case "h3": {
const text = getTextContent(node);
// Set colors based on heading level
if (node.type === "h1") {
term.setTextColor(colors.yellow);
} else if (node.type === "h2") {
term.setTextColor(colors.orange);
} else if (node.type === "h3") {
term.setTextColor(colors.lightGray);
} else {
term.setTextColor(colors.white);
// Set colors based on heading level (if not overridden by styleProps)
if (textColor === undefined) {
if (node.type === "h1") {
textColor = colors.yellow;
} else if (node.type === "h2") {
textColor = colors.orange;
} else if (node.type === "h3") {
textColor = colors.lightGray;
} else {
textColor = colors.white;
}
}
term.setBackgroundColor(colors.black);
term.setTextColor(textColor);
term.setBackgroundColor(bgColor ?? colors.black);
term.setCursorPos(x, y);
term.write(text.substring(0, width));
@@ -70,13 +79,13 @@ function drawNode(node: UIObject, focused: boolean): void {
case "button": {
const text = getTextContent(node);
// Set colors based on focus
// Set colors based on focus (if not overridden by styleProps)
if (focused) {
term.setTextColor(colors.black);
term.setBackgroundColor(colors.yellow);
term.setTextColor(textColor ?? colors.black);
term.setBackgroundColor(bgColor ?? colors.yellow);
} else {
term.setTextColor(colors.white);
term.setBackgroundColor(colors.gray);
term.setTextColor(textColor ?? colors.white);
term.setBackgroundColor(bgColor ?? colors.gray);
}
term.setCursorPos(x, y);
@@ -96,11 +105,11 @@ function drawNode(node: UIObject, focused: boolean): void {
}
if (focused) {
term.setTextColor(colors.black);
term.setBackgroundColor(colors.white);
term.setTextColor(textColor ?? colors.black);
term.setBackgroundColor(bgColor ?? colors.white);
} else {
term.setTextColor(colors.white);
term.setBackgroundColor(colors.black);
term.setTextColor(textColor ?? colors.white);
term.setBackgroundColor(bgColor ?? colors.black);
}
term.setCursorPos(x, y);
@@ -112,33 +121,71 @@ function drawNode(node: UIObject, focused: boolean): void {
if (typeof valueProp === "function") {
value = (valueProp as Accessor<string>)();
}
const placeholder = node.props.placeholder as string | undefined;
const cursorPos = node.cursorPos ?? 0;
let displayText = value;
if (value === "" && placeholder !== undefined) {
let currentTextColor = textColor;
let showPlaceholder = false;
const focusedBgColor = bgColor ?? colors.white;
const unfocusedBgColor = bgColor ?? colors.black;
if (value === "" && placeholder !== undefined && !focused) {
displayText = placeholder;
term.setTextColor(colors.gray);
showPlaceholder = true;
currentTextColor = currentTextColor ?? colors.gray;
} else if (focused) {
term.setTextColor(colors.black);
currentTextColor = currentTextColor ?? colors.black;
} else {
term.setTextColor(colors.white);
currentTextColor = currentTextColor ?? colors.white;
}
if (focused) {
term.setBackgroundColor(colors.white);
} else {
term.setBackgroundColor(colors.black);
}
// Set background and clear the input area, creating a 1-character padding on the left
term.setBackgroundColor(focused ? focusedBgColor : unfocusedBgColor);
term.setCursorPos(x, y);
// Pad or truncate to fit width
if (displayText.length > width) {
displayText = displayText.substring(0, width);
} else {
displayText = displayText.padEnd(width, " ");
term.write(" ".repeat(width));
term.setTextColor(currentTextColor);
term.setCursorPos(x + 1, y); // Position cursor for text after padding
const renderWidth = width - 1;
let textToRender = displayText;
// Truncate text if it's too long for the padded area
if (textToRender.length > renderWidth) {
textToRender = textToRender.substring(0, renderWidth);
}
if (focused && !showPlaceholder && cursorBlinkState) {
// Draw text with a block cursor by inverting colors at the cursor position
for (let i = 0; i < textToRender.length; i++) {
const char = textToRender.substring(i, i + 1);
if (i === cursorPos) {
// Invert colors for cursor
term.setBackgroundColor(currentTextColor);
term.setTextColor(focusedBgColor);
term.write(char);
// Restore colors
term.setBackgroundColor(focusedBgColor);
term.setTextColor(currentTextColor);
} else {
term.write(char);
}
}
// Draw cursor at the end of the text if applicable
if (cursorPos === textToRender.length && cursorPos < renderWidth) {
term.setBackgroundColor(currentTextColor);
term.setTextColor(focusedBgColor);
term.write(" ");
// Restore colors
term.setBackgroundColor(focusedBgColor);
term.setTextColor(currentTextColor);
}
} else {
// Not focused or no cursor, just write the text
term.write(textToRender);
}
term.write(displayText);
}
break;
}
@@ -147,7 +194,16 @@ function drawNode(node: UIObject, focused: boolean): void {
case "form":
case "for":
case "show": {
// Container elements don't draw themselves, just their children
// Container elements may have background colors
if (bgColor !== undefined && node.layout !== undefined) {
const { x: divX, y: divY, width: divWidth, height: divHeight } = node.layout;
term.setBackgroundColor(bgColor);
// Fill the background area
for (let row = 0; row < divHeight; row++) {
term.setCursorPos(divX, divY + row);
term.write(string.rep(" ", divWidth));
}
}
break;
}
@@ -158,8 +214,8 @@ function drawNode(node: UIObject, focused: boolean): void {
? (node.textContent)()
: node.textContent;
term.setTextColor(colors.white);
term.setBackgroundColor(colors.black);
term.setTextColor(textColor ?? colors.white);
term.setBackgroundColor(bgColor ?? colors.black);
term.setCursorPos(x, y);
term.write(text.substring(0, width));
}
@@ -177,15 +233,16 @@ function drawNode(node: UIObject, focused: boolean): void {
*
* @param node - The root node to render
* @param focusedNode - The currently focused node (if any)
* @param cursorBlinkState - Whether the cursor should be visible (for blinking)
*/
export function render(node: UIObject, focusedNode?: UIObject): void {
export function render(node: UIObject, focusedNode?: UIObject, cursorBlinkState = false): void {
// Draw this node
const isFocused = node === focusedNode;
drawNode(node, isFocused);
drawNode(node, isFocused, cursorBlinkState);
// Recursively draw children
for (const child of node.children) {
render(child, focusedNode);
render(child, focusedNode, cursorBlinkState);
}
}

View File

@@ -24,13 +24,13 @@ const Counter = () => {
const [count, setCount] = createSignal(0);
return div(
{ class: "flex flex-col" },
{ class: "flex flex-col bg-blue" },
h3("Counter Example"),
label({}, () => `Count: ${count()}`),
label({ class: "text-yellow" }, () => `Count: ${count()}`),
div(
{ class: "flex flex-row" },
button({ onClick: () => setCount(count() - 1) }, "-"),
button({ onClick: () => setCount(count() + 1) }, "+"),
button({ onClick: () => setCount(count() - 1), class: "text-red" }, "-"),
button({ onClick: () => setCount(count() + 1), class: "text-green" }, "+"),
),
);
};
@@ -67,7 +67,7 @@ const TodosApp = () => {
onInput: setNewTitle,
placeholder: "Enter new todo",
}),
button({ onClick: addTodo }, "Add"),
button({ onClick: addTodo, class: "bg-green text-white" }, "Add"),
),
For({ each: todos, class: "flex flex-col" }, (todo, index) =>
div(
@@ -79,10 +79,15 @@ const TodosApp = () => {
setTodos(index(), "completed", checked);
},
}),
label({ class: "ml-1" }, () => todo.title),
label(
{
class: todo.completed ? "ml-1 text-gray" : "ml-1 text-white"
},
() => todo.title
),
button(
{
class: "ml-1",
class: "ml-1 bg-red text-white",
onClick: () => {
setTodos((t) => removeIndex(t, index()));
},