Docs menu

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 in s seconds.

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 suspended and waits until the UI thread has let go before it returns. Stop drawing until resumed.
  • On resumed, keep the Renderer and call renderer.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.