跳转到内容

计数器教程

计数器是第一条正式教程。我们让一个数字每秒醒来一次,它会带你走过 ratatui-kit 的核心路径:组件更新、hook 保存状态、异步任务写入 state、waker 唤醒下一帧。

counter demo

这个录屏由 docs/tapes/counter.tape 生成,运行的是仓库里的真实 example。

cargo run --example counter

q 退出,或使用 Ctrl+C 强制退出。

完整源码在 examples/start/counter.rs。下面是组件主体:

#[component]
fn Counter(mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
    let mut count = hooks.use_state(|| 0_u64);
    hooks.use_future(async move {
        loop {
            tokio::time::sleep(std::time::Duration::from_secs(1)).await;
            count += 1;
        }
    });

    let mut exit = hooks.use_exit();
    hooks.use_event_handler(EventScope::Current, EventPriority::Normal, move |event| {
        let Event::Key(key) = event else {
            return EventResult::Ignored;
        };
        if key.kind == KeyEventKind::Press
            && matches!(key.code, KeyCode::Char('q') | KeyCode::Char('Q'))
        {
            exit();
            return EventResult::Consumed;
        }

        EventResult::Ignored
    });

    element!(
        Center(
            width: Constraint::Length(48),
            height: Constraint::Length(9),
        ) {
            Border(
                flex_direction: Direction::Vertical,
                justify_content: Flex::Center,
                border_style: Style::new().cyan(),
                top_title: Line::from(" ratatui-kit counter ").cyan().bold().centered(),
                bottom_title: Line::from(" q quit · Ctrl+C exit ").dark_gray().centered(),
            ){
                View(
                    flex_direction: Direction::Vertical,
                    justify_content: Flex::Center,
                    gap: 1,
                ) {
                    View(height: Constraint::Length(1)){
                        Text(text: Line::styled(
                            format!("Counter: {:02}", count.get()),
                            Style::new().green().bold(),
                        ).centered())
                    }
                    View(height: Constraint::Length(1)){
                        Text(text: Line::from("state writes wake the terminal UI").centered())
                    }
                }
            }
        }
    )
}

#[component]Counter 变成一个持久 UI 单元。函数每帧都会重新执行,但 hooks 挂在组件实例上,不会随着函数返回而消失。

use_state 创建局部状态:

let mut count = hooks.use_state(|| 0_u64);

count 是一个可复制的响应式句柄。它不是普通局部变量;写入它会唤醒运行时。

use_future 在组件挂载时启动一次异步任务:

hooks.use_future(async move {
    loop {
        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
        count += 1;
    }
});

每次 count += 1 都会标记状态变更。渲染循环正在等待 “组件树变化或终端事件”,于是它被唤醒,重新运行组件函数,再把新的数字画到终端。

element! 描述本帧 UI:

element!(
    Center(width: Constraint::Length(48), height: Constraint::Length(9)) {
        Border {
            Text(text: format!("Counter: {:02}", count.get()))
        }
    }
)

这段概念代码省略了样式和内部布局,只保留 element tree 的轮廓。这里的 Element 是声明,廉价且每帧重建;真正保存状态的是运行时复用的组件实例。

这个例子把框架最小闭环跑了一遍:

  • 入口从 element!(Counter).fullscreen().await 开始。
  • 组件函数负责声明 UI。
  • hooks 负责跨帧保存状态和副作用。
  • 响应式状态负责唤醒下一帧。
  • ratatui buffer 只在 draw 阶段被写入。

下一步看 异步数据三态,把 “定时自增” 换成 “请求、loading、error 和数据刷新”。