The app runtime
anvil_ui::runner runs your app on its own UI thread, draws only when
something changes, paces animation by the display, and handles the Android lifecycle.
Two threads
The operating system requires windows to belong to the main thread, and Windows blocks that thread while the user holds or drags the title bar or resizes the window. So the runner splits the work:
- The event thread (main) creates the window and forwards its events. Nothing else.
- The UI thread owns your app: it handles events, builds frames, renders, and waits for the display. A title-bar drag cannot stop it.
Your app is created on the UI thread (App::new runs there), so it does not need to be
Send.
The App trait
pub trait App {
type Event: Send + 'static;
fn resumed(&mut self, window: &'static Window); // make (or remake) the renderer's surface
fn suspended(&mut self); // Android took the surface away
fn window_event(&mut self, event: WindowEvent, cx: &mut Cx);
fn user_event(&mut self, event: Self::Event, cx: &mut Cx);
fn about_to_wait(&mut self, cx: &mut Cx); // after events and after each frame
fn frame(&mut self, cx: &mut Cx); // build and draw one frame
fn exiting(&mut self); // save what needs saving
}
Cx is what you ask of the runner: cx.request_redraw() (draw after
the waiting events), cx.wake_at(instant) (wake without an event, for a timer or a
caret blink) and cx.exit().
Frames on demand
The UI thread blocks until an event arrives or a wake is due, so an idle app uses no CPU. After
each frame, ui.next_wake() says when the next one is needed:
None: nothing is moving; sleep until an event.Some(0.0): an animation is running; draw again right away. Presenting waits for the display, so this runs at the display's refresh rate, not at a fixed 60 Hz.Some(s): a timer, such as the caret blink; wake insseconds.
When a window is minimized nothing waits for the display, so render returns
FrameStatus::Skipped at once. Back off then (the starter waits 100 ms), or a running
animation would spin a CPU core.
Live resize
On a resize, the event thread waits (at most 100 ms) until the UI thread has drawn a frame at the
new size. The window's edge and its content move together; without this, Windows stretches the
old frame across the new size while you drag. Handle WindowEvent::Resized by calling
renderer.resize(width, height); the runner asks for the frame.
Workers
Do slow work (network, disk, decoding) on your own threads, and send results back through the
Proxy your app was created with. A send wakes the UI thread at once, with no polling:
pub enum Event { Loaded(Vec<Item>) }
impl App {
pub fn new(proxy: Proxy<Event>) -> Self {
std::thread::spawn(move || {
let items = fetch_items(); // blocking is fine here
proxy.send(Event::Loaded(items)); // wakes the UI thread
});
/* ... */
}
}
impl runner::App for App {
type Event = Event;
fn user_event(&mut self, event: Event, cx: &mut Cx) {
match event {
Event::Loaded(items) => self.items = items,
}
cx.request_redraw();
}
/* ... */
}
proxy.send returns false once the app has stopped, so a worker can
end then.
The Android lifecycle
- When the app leaves the screen, Android destroys its surface. The runner calls
suspendedand waits until the UI thread has let go before it returns. Stop drawing untilresumed. - On
resumed, keep theRendererand callrenderer.recreate_surface(window, w, h). Textures and glyphs survive. - Save state in
suspended: Android may end the process without warning afterwards. - Declare every configuration change in the manifest (see Platforms). ANVL lays itself out again every frame; left to Android, a rotation recreates the activity.