State, Focus, Input
Ansiq deliberately separates:
- reactive state
- focus ownership
- key routing
- continuity across subtree replacement
Continue the NotesApp
Now we extend the same example again. Instead of only input and submit, it becomes:
- a list of saved notes on the left
- an input area on the right
#[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))}
/>
}
}Now abstract runtime ideas become visible:
Listneeds selection stateInputneeds cursor stateTabneeds to move between widgets- subtree replacement must not destroy those states
There is also an important state split here:
draftstill represents in-progress local inputnotesandselectedare app-level state
That is why focus and continuity now matter more.
Once one screen has multiple interactive regions, storing values is no longer the whole story.
Why focus belongs to the runtime
Focus is not just a widget-local concern.
That means:
Tab/Shift-Tabmust be handled consistently- focus traversal has one owner
- unhandled keys can bubble back to the app
- focus scopes can constrain navigation inside a subtree
Minimal focus trap usage
When one region should own Tab traversal temporarily, establish a focus scope explicitly:
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();
}
_ => {}
}
}Here "composer-panel" is a stable continuity-key scope, not a temporary node id.
Why continuity matters
Without continuity, this version of NotesApp would quickly break:
- the input cursor would reset
- list selection would disappear
- focus would drift after subtree replacement
That is why continuity is part of the runtime architecture, not a widget afterthought.
How unhandled keys work
Not every key should be consumed by a widget.
If the focused widget does not consume a key, and the runtime also does not consume it for focus navigation, the app can still react to it:
fn on_unhandled_key(
&mut self,
key: Key,
handle: &RuntimeHandle<Message>,
) -> bool {
if key == Key::Esc {
let _ = handle.emit(Message::Cancel);
return true;
}
false
}This is a good fit for:
- global shortcuts
- cancel / close behavior
- mode changes
- command entry
The deeper point of this page is:
- state answers “what is the value”
- focus answers “who is receiving input”
- routing answers “who should consume this key”
- continuity answers “what survives rerender”
Next
Continue with Streaming and Async Tasks.
We keep extending the same example, this time with background work and chunked updates, before we return to layout and rendering.