状态、焦点与输入
在 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需要 cursorTab需要在两个控件之间切换- subtree replacement 之后这些状态不能随便丢
这里还有一个很重要的状态分层:
draft继续表示“当前正在输入、尚未提交”的局部状态notes和selected则是 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