Skip to content

The backend contract

What xpui needs from whatever hosts it, as five traits and the functions that install them. The framework never talks to a backend directly: a host implements host::Canvas, host::TextMetrics, host::Chrome, host::InputSource and host::Clock on one type, which makes it a host::Host, and hands a 'static reference to host::install before the first frame. None of these names is re-exported at the crate root, so a backend writes xpui::host::Canvas, and a screen never has a reason to.

The host contract explains what each trait owes the framework and what is easy to get wrong. Writing a backend walks through building one, a trait at a time. This page is the Host object, input, the clock and installation, method by method; the three drawing traits are Canvas and chrome. The navigation trait a shell installs beside the host is Navigator.

host::HostEverything a backend must provide, as one object implementing all five traits.
host::InputSourceOne frame of input, and what the device it came from can do.
host::ClockThe host's monotonic clock.
host::installInstalls the host, before any view is measured or drawn.
host::is_installedWhether a host has been installed, so tests can assert wiring.
host::install_navigatorInstalls the navigator, before the first frame.
host::is_navigator_installedWhether a navigator has been installed, so a host can assert its wiring and stay idempotent across two entry points.
ImplementationCanvas, TextMetrics, InputSource, ClockChrome
xpui-embedded-graphicsby hand, over a DrawTargetfrom xpui_chrome::plain_chrome!, painted with its own Canvas
xpui-fuiby hand, across a C ABI into FreeInkUIby hand, the same way
the fake host, xpui::testing::TestHostby hand, recording every callby hand, recording every call

Every method takes &self. The framework holds one &'static dyn Host and calls it from wherever a frame runs, so a host that keeps per-frame state keeps it behind interior mutability, and the whole type must be Sync.

Everything a backend must provide, as one object implementing all five traits.

pub trait Host: Canvas + TextMetrics + Chrome + InputSource + Clock + Sync

A host installs a single value and the framework keeps one pointer. Nothing implements Host by hand: a blanket implementation covers every Sync type that implements the five, so a missing method shows up as a missing trait.

None of its own. Every method belongs to one of the five supertraits.

None.

Example — a skeleton host

Every method answers "nothing": no fonts, no icons, no input, and paint that goes nowhere. It is the shape a new backend starts from, and it installs.

Rust
use xpui::host::{
self, Button, Canvas, Chrome, Clock, ControlState, FontId, FontRole, FontStyle, Hint,
IconRef, InputSource, RowField, TextMetrics, ThemeMetric,
};
use xpui::{Point, Rect, Size, SwipeDir};
struct Skeleton;
impl Canvas for Skeleton {
fn screen_size(&self) -> Size { Size::new(480, 800) }
fn clear(&self) {}
fn draw_text(&self, _origin: Point, _text: &str, _font: FontId, _style: FontStyle) {}
fn fill_rect(&self, _rect: Rect, _black: bool) {}
fn stroke_rect(&self, _rect: Rect) {}
fn draw_line(&self, _from: Point, _to: Point) {}
fn fill_rect_dither(&self, _rect: Rect, _light: bool) {}
fn scrim(&self, _rect: Rect) {}
fn set_clip(&self, _rect: Option<Rect>) {}
fn draw_image(&self, _origin: Point, _data: &[u8], _size: Size) {}
fn draw_icon(&self, _origin: Point, _icon: IconRef) {}
fn icon_size(&self, _icon: IconRef) -> i32 { 0 }
}
impl TextMetrics for Skeleton {
fn font(&self, _role: FontRole) -> FontId { FontId::UNAVAILABLE }
fn text_width(&self, _font: FontId, _text: &str, _style: FontStyle) -> i32 { 0 }
fn line_height(&self, _font: FontId) -> i32 { 0 }
}
impl Chrome for Skeleton {
fn metric(&self, _metric: ThemeMetric) -> i32 { 0 }
fn draw_header(&self, _title: Option<&str>, _subtitle: Option<&str>) {}
fn draw_sub_header(&self, _rect: Rect, _label: &str, _right_label: Option<&str>) {}
fn draw_button_hints(&self, _back: &Hint, _confirm: &Hint, _previous: &Hint, _next: &Hint) {}
fn draw_progress_bar(&self, _rect: Rect, _current: u32, _total: u32) {}
fn draw_slider(&self, _rect: Rect, _value: i32, _max: i32, _state: ControlState) {}
fn draw_scroll_indicator(&self, _rect: Rect, _content: i32, _visible: i32, _offset: i32) {}
fn draw_list<'a>(
&self,
_rect: Rect,
_rows: usize,
_selected: i32,
_row: &dyn Fn(usize, RowField) -> Option<&'a str>,
) {
}
fn draw_option_popup<'a>(
&self,
_title: &str,
_options: &dyn Fn(usize) -> Option<&'a str>,
_count: usize,
_selected: i32,
) {
}
fn option_popup_row_rect<'a>(
&self,
_title: &str,
_options: &dyn Fn(usize) -> Option<&'a str>,
_count: usize,
_index: usize,
) -> Option<Rect> {
None
}
fn request_update(&self) {}
}
impl InputSource for Skeleton {
fn was_pressed(&self, _button: Button) -> bool { false }
fn is_pressed(&self, _button: Button) -> bool { false }
fn was_released(&self, _button: Button) -> bool { false }
fn has_touch(&self) -> bool { false }
fn has_left_right_keys(&self) -> bool { false }
fn tap(&self) -> Option<Point> { None }
fn touch_held(&self) -> Option<Point> { None }
fn touch_released(&self) -> bool { false }
fn swipe(&self) -> SwipeDir { SwipeDir::None }
fn was_back_gesture(&self) -> bool { false }
fn was_home_gesture(&self) -> bool { false }
}
impl Clock for Skeleton {
fn millis(&self) -> u32 { 0 }
}
// Caught at the definition, not at the install site, if a trait is missing.
const _: fn() = || {
fn assert_host<T: host::Host>() {}
assert_host::<Skeleton>();
};
static BACKEND: Skeleton = Skeleton;
// Safety: this is the only thread, and no frame has started.
unsafe { host::install(&BACKEND) };
assert_eq!(xpui::Renderer::screen_size(), Size::new(480, 800));
assert_eq!(xpui::Font::ui().text_width("Wi-Fi"), 0, "no font, so no width");

See also: host::install, the host contract

One frame of input, and what the device it came from can do.

pub trait InputSource

Edge queries are true for exactly one frame. Nothing is consumed by reading, so the framework may ask the same question more than once per frame. A host latches what its event loop saw at the top of a frame, and answers from that until the next one.

One method is not about a frame at all: host::InputSource::has_left_right_keys describes the device, and answers the same every time. A screen reaches the rest through Input.

Whether button went down this frame.

fn was_pressed(&self, button: Button) -> bool

An edge. The runtime starts key auto-repeat from it, timed against host::Clock::millis. button is a Button, by meaning: a host maps its physical keys, and the user's remapping, onto it.

Whether button is down, this frame included.

fn is_pressed(&self, button: Button) -> bool

A level, true on the frame was_pressed is and on every frame after until the key comes up. Auto-repeat reads it.

Whether button came up this frame.

fn was_released(&self, button: Button) -> bool

Whether this frame carries any touch at all, down or just lifted.

fn has_touch(&self) -> bool

About this frame, never whether the panel has a digitiser. A device with no touch panel answers false every frame.

Whether the device has a Left/Right pair to nudge a value with.

fn has_left_right_keys(&self) -> bool

With the pair, Left and Right move a value where it stands. Without one, those keys are busy walking between rows, so Confirm opens the value and closes it again. A control reads this and branches; nothing in the framework does so on its behalf.

A completed tap, at the position the finger went down.

fn tap(&self) -> Option<Point>

Some on the one frame the tap completes, and None on every other.

Where a finger is while one is down, the signal a slider drag needs.

fn touch_held(&self) -> Option<Point>

A level: Some on every frame the finger stays down, with its current position, and None once it lifts.

Whether a finger lifted this frame.

fn touch_released(&self) -> bool

A completed swipe, or SwipeDir::None.

fn swipe(&self) -> SwipeDir

An edge, like a tap: a SwipeDir on the one frame the swipe completes, and SwipeDir::None otherwise.

The system back gesture, an edge swipe on a touch device.

fn was_back_gesture(&self) -> bool

The runtime takes it as a press of Button::Back, offered to the screen first.

The system home gesture, offered to the screen before the host acts.

fn was_home_gesture(&self) -> bool

App::tick reads it every frame, so a host reports it here and calls nothing else. The screen on top sees it first, through Screen::handle_home_gesture, and consumes it by returning true, as an overlay does to dismiss itself.

Which way a vertical swipe moves focus.

fn swipe_moves_selection(&self) -> bool

false, the default, means the swipe moves the content: swiping up walks down the list, as though dragging the page. true moves the focus with the swipe. Defaulted so a host need not implement it until there is a setting behind it.

See also: Input, Button, SwipeDir

The host's monotonic clock.

pub trait Clock

Anything the framework paces itself with reads this. A screen reads it through xpui::millis.

Milliseconds since boot.

fn millis(&self) -> u32

Key auto-repeat is timed against it. The caller only takes wrapping differences, so a host may return a plain counter that wraps. It must be a real clock, though: answering 0 for ever means a held key never repeats.

Example — a counter a timer advances

Rust
use core::sync::atomic::{AtomicU32, Ordering};
use xpui::host::Clock;
/// Milliseconds, advanced by the board's timer interrupt.
struct Ticks(AtomicU32);
impl Ticks {
fn advance(&self, elapsed: u32) {
self.0.fetch_add(elapsed, Ordering::Relaxed); // wraps at u32::MAX
}
}
impl Clock for Ticks {
fn millis(&self) -> u32 {
self.0.load(Ordering::Relaxed)
}
}
static CLOCK: Ticks = Ticks(AtomicU32::new(u32::MAX - 4));
let before = CLOCK.millis();
CLOCK.advance(10);
assert!(CLOCK.millis() < before, "the counter wrapped");
assert_eq!(CLOCK.millis().wrapping_sub(before), 10, "the difference did not");

Installs the host, before any view is measured or drawn.

pub unsafe fn install(host: &'static dyn Host)
ParameterMeaning
hostThe host, for the rest of the process. A static, or a value leaked with Box::leak.

Installing again replaces it — another panel size is another backend — and the old &'static stays valid for anything that read it.

The host is process-wide. A test that installs a second corrupts what the first was serving, which shows up as flakiness. Put tests that install a real backend in their own integration test file, which Cargo runs as its own process, or take the lock xpui::testing::Ui holds.

Nothing reads the host before a view exists, so installing first thing in the entry point, before the render loop starts, satisfies the contract. In a build with the testing feature, a read that finds no host installs the fake instead of panicking; without it, that read panics with "xpui::host::install was never called".

Example — installing from an entry point

Rust
use xpui::host;
use xpui::testing::TestHost;
static BACKEND: TestHost = TestHost;
fn main() {
// Safety: this is the only thread, and nothing has been measured, rendered
// or asked for its interactions yet.
unsafe { host::install(&BACKEND) };
assert!(host::is_installed());
// A host built at run time lives for the process, too.
let built: &'static TestHost = Box::leak(Box::new(TestHost));
// Safety: still the only thread, and still no frame in flight.
unsafe { host::install(built) };
}

See also: host::is_installed, host::install_navigator, writing a backend

Whether a host has been installed, so tests can assert wiring.

pub fn is_installed() -> bool

xpui::testing::install reads it to stay idempotent: it installs the fake only when nothing is installed, so it never takes the global back off a real backend a test installed first.

Example — the fake host, installed

Rust
use xpui::{host, testing};
testing::install();
assert!(host::is_installed());
assert!(host::is_navigator_installed(), "the fake is a navigator as well");
testing::install(); // idempotent: the host already there stays
assert!(host::is_installed());

Installs the navigator, before the first frame.

pub unsafe fn install_navigator(navigator: &'static dyn Navigator)

The second installable thing, apart from the host because it has a different owner: a backend crate supplies the host, and whoever owns the screen stack supplies the Navigator. App::new installs itself, and a C++ firmware installs one that calls across its FFI. With none installed, present and finish_screen do nothing, and a debug build says so.

Example — installing from two entry points

Rust
use xpui::host;
use xpui::testing::TestHost;
static SHELL: TestHost = TestHost;
/// Called first thing by both the render task and the input task.
fn wire_navigation() {
if !host::is_navigator_installed() {
// Safety: no frame has started on either task, and the two entry
// points do not run at the same time.
unsafe { host::install_navigator(&SHELL) };
}
}
wire_navigation();
wire_navigation(); // the second finds it installed, and writes nothing
assert!(host::is_navigator_installed());

Whether a navigator has been installed, so a host can assert its wiring and stay idempotent across two entry points.

pub fn is_navigator_installed() -> bool

See also: host::install_navigator, Navigator