Skip to Content
指南状态、焦点与输入

状态、焦点与输入

在 Ansiq 里,输入相关的职责是明确拆开的:

  • 响应式状态负责“值是什么”
  • runtime 负责“谁有焦点”
  • routing 负责“哪个 widget 消费这个按键”
  • continuity contract 负责“子树替换后哪些交互态要保留”

继续沿用 NotesApp

现在我们继续扩充同一个示例,把它从“输入 + 提交”升级成“左侧 note 列表 + 右侧输入区”。

#[derive(Clone, Debug)] enum Message { Submit(String), Select(usize), } #[derive(Default)] struct NotesApp { notes: Vec<String>, selected: Option<usize>, submitted: String, } fn render(&mut self, cx: &mut ViewCtx<'_, Message>) -> Element<Message> { let draft = cx.signal(|| String::new()); let current = draft.get(); view! { <List items={self.notes.clone()} selected={self.selected} on_select={|idx| Some(Message::Select(idx))} /> <Input value={current.clone()} on_change={{ let draft = draft.clone(); move |next| draft.set_if_changed(next) }} on_submit={|next| Some(Message::Submit(next))} /> } }

这个版本让很多抽象概念终于有了落点:

  • List 需要选中态
  • Input 需要 cursor
  • Tab 需要在两个控件之间切换
  • subtree replacement 之后这些状态不能随便丢

这里还有一个很重要的状态分层:

  • draft 继续表示“当前正在输入、尚未提交”的局部状态
  • notesselected 则是 app 级状态

这也是为什么焦点和 continuity contract 会开始变得重要。
一旦同一屏里出现多个交互区域,单纯“把值存起来”已经不够了。

焦点属于 runtime

焦点不是某个 widget 的私有行为。

这意味着:

  • Tab / Shift-Tab 有统一的处理逻辑
  • focus traversal 有统一所有者
  • 未消费按键可以回到 app 层
  • focus trap 可以限制在某个子树作用域里

focus trap 的最小用法

当某个区域应该独占 Tab 导航时,可以显式建立焦点作用域:

fn update(&mut self, message: Message, handle: &RuntimeHandle<Message>) { match message { Message::OpenComposer => { let _ = handle.trap_focus_in("composer-panel"); } Message::CloseComposer => { let _ = handle.clear_focus_scope(); } _ => {} } }

这里的 "composer-panel" 不是临时 node id,而是 continuity key 对应的稳定作用域标识。

continuity contract 在这个例子里意味着什么

如果 continuity contract 不成立,这个版本的 NotesApp 会很快出问题:

  • 输入框 cursor 在 rerender 后跳掉
  • 列表选中项被重置
  • focus 在组件重排后跑丢

所以 continuity contract 不是后补优化,而是 subtree replacement 能否稳定工作的前提。

unhandled key 的作用

并不是每个按键都应该被 widget 吞掉。

当 widget 没消费、runtime 的全局焦点路由也没消费时,app 仍然可以收到未消费按键。
这对这些场景很重要:

  • 全局快捷键
  • Escape 退出或取消
  • 模式切换
  • 命令入口
fn on_unhandled_key( &mut self, key: Key, handle: &RuntimeHandle<Message>, ) -> bool { if key == Key::Esc { let _ = handle.emit(Message::Cancel); return true; } false }

这个钩子的语义是:

  • 先让 focused widget 处理
  • 再让 runtime 做全局焦点导航
  • 最后才回到 app

所以这一页真正想说明的是:

  • state 只负责“值是什么”
  • focus 决定“谁在接收输入”
  • routing 决定“这个按键应该交给谁”
  • continuity 决定“rerender 之后交互态还能不能接上”

下一步

继续阅读 流式输出与异步任务
下一页我们会继续沿用同一个例子,加入后台任务和分段返回的内容,再看看这些更新是如何进入 runtime 的。

Last updated on