Skip to Content
GuideState, Focus, Input

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:

  • List needs selection state
  • Input needs cursor state
  • Tab needs to move between widgets
  • subtree replacement must not destroy those states

There is also an important state split here:

  • draft still represents in-progress local input
  • notes and selected are 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-Tab must 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.

Last updated on