Skip to content

The embedded-graphics backend

The host itself: a Backend over any embedded-graphics DrawTarget, the input a caller feeds it each frame, the two colours ink and background land as, and the wrapper a parallel-bus panel needs. Everything a screen draws goes through one of these, and everything it hears about buttons and fingers comes in through one.

The crate's README wires one into a loop from nothing, and design.md says why it has the shape it does. This page is what each piece does.

BackendAn xpui host over an embedded-graphics display.
DisplayLoanA display that has been taken out of its backend, and goes back on drop.
InputStateOne frame of input, as the event source writes it and the framework reads it.
PacedFillWraps a display, replacing its repeated-pixel path with a per-pixel one.
PaletteThe two colours ink and background map onto.
Re-exportsDrawTarget, Labels and Metrics, from the crates this one is built on.

An xpui host over an embedded-graphics display.

pub struct Backend<D: DrawTarget>

D is the display: a panel driver, an OLED, a colour TFT, or xpui_screenshot::Framebuffer in a test. The backend supplies Canvas and TextMetrics itself, InputSource and Clock from what the caller feeds it, and Chrome from xpui-chrome, so a list, a dialog and a slider look like something without anyone drawing one.

Canvas::clear fills the whole panel and ignores any clip. It writes the background straight to the display, however small a clip is set at the time; every other primitive draws through the clip.

BuilderSetsWhen not called
with_metricsthe measurements chrome is painted toMetrics::DEFAULT
with_fontsthe family, and the size each role wantsFonts::DEFAULT
with_labelsthe words in the hint barLabels::ENGLISH
with_keyswhat the keys along the bottom edge meanKeyRow::READER
with_left_right_keyswhether the device has a Left/Right pairfalse

The caller drives the frame. A loop calls begin_frame with the clock, reports whatever input arrived, ticks the App, renders if a repaint was asked for, and pushes the pixels with with_display or loan_display.

Example — a frame, driven by hand

Rust
use xpui::{App, Button, NavigationScreen, Screen, Text, View};
use xpui_eg::{Backend, Palette};
use xpui_screenshot::Framebuffer;
struct Hello;
impl Screen for Hello {
type Message = ();
fn body(&self) -> impl View<()> {
NavigationScreen::new(Text::new("Hello"))
}
fn update(&mut self, _message: ()) {}
}
let backend = Backend::leak(Framebuffer::new(480, 800), Palette::INK_IS_ON);
// Safety: one thread, and nothing has rendered on this backend yet.
unsafe { xpui::host::install(backend) };
let mut app = App::new(Hello);
backend.begin_frame(0);
backend.press(Button::Down); // from wherever input comes from
app.tick();
if app.render_if_dirty() {
backend.clear_dirty();
backend.with_display(|frame| assert!(frame.ink_count() > 0));
}

Example — a small panel, configured

Rust
use xpui::host::KeyRow;
use xpui_eg::{Backend, Fonts, Labels, Metrics, Palette};
use xpui_screenshot::Framebuffer;
let backend = Backend::new(Framebuffer::new(296, 128), Palette::INK_IS_OFF)
.with_metrics(Metrics::SMALL)
.with_fonts(Fonts::SMALL)
.with_labels(Labels::ENGLISH_SHORT)
.with_keys(KeyRow::READER)
.with_left_right_keys(true);
assert!(backend.left_right_keys());
assert_eq!(backend.fonts().ui, Fonts::SMALL.ui);

A backend over display, painting ink and background as palette says, with the default metrics, English labels and a reader's key row.

pub fn new(display: D, palette: Palette<D::Color>) -> Self

The panel's size is read once, here, so it can still be answered while the display is on loan.

Leaks the backend so it can be installed.

pub fn leak(display: D, palette: Palette<D::Color>) -> &'static Self where D: 'static,

xpui::host::install takes a &'static, and a backend lives as long as the program that draws with it, so this is one allocation that was never going to be freed anyway.

Leaks a backend a caller has finished building.

pub fn leaked(self) -> &'static Self where D: 'static,

leak builds and leaks in one step, which leaves nowhere for the builders. This is the other order: Backend::new(display, palette).with_metrics(m).leaked().

Paints chrome to these measurements instead of the default.

pub fn with_metrics(self, metrics: Metrics) -> Self

Per backend rather than global, because a 296x128 panel and a 480x800 one need different numbers, and a process can drive both.

Sets the type the framework's roles resolve through: a family, and the size each role wants.

pub fn with_fonts(self, fonts: Fonts) -> Self

For a backend nothing will re-set. set_family replaces it while running. See Fonts.

The words its hint bar shows.

pub fn with_labels(self, labels: Labels) -> Self

English until an application says so.

What the keys along the device's bottom edge mean, left to right.

pub fn with_keys(self, keys: KeyRow) -> Self

Says the device has a Left/Right pair.

pub fn with_left_right_keys(self, present: bool) -> Self

A fact about the hardware, so it is told rather than derived: the backend has no idea what is around the display it was handed.

The measurements every chrome painter lays out against.

pub fn metrics(&self) -> &Metrics

The type in use, as with_fonts or a switch left it.

pub fn fonts(&self) -> Fonts

The words this backend paints hints with.

pub fn labels(&self) -> &Labels

What the keys along the device's bottom edge mean.

pub fn keys(&self) -> &KeyRow

Whether it was told the device has a Left/Right pair.

pub fn left_right_keys(&self) -> bool

Sets the type in another family, and asks for a repaint.

pub fn set_family(&self, family: &'static Family)

The sizes are re-derived from the chrome, not carried over: each role keeps the line height these metrics call for, and the new family answers with the tier it was cut in. Every font id changes with it, since an id is a hash of the bytes. A screen that wants every backend to follow calls request_family instead.

Rust
use xpui_eg::{Backend, Fonts, HELVETICA, Palette};
use xpui_screenshot::Framebuffer;
let backend = Backend::new(Framebuffer::new(480, 800), Palette::INK_IS_ON);
backend.clear_dirty();
backend.set_family(&HELVETICA);
assert!(backend.is_dirty());
assert_eq!(backend.fonts().ui, Fonts::for_metrics(backend.metrics()).ui);

Starts a frame: advances the clock and clears one-frame input.

pub fn begin_frame(&self, millis: u32)

A family a screen asked for with request_family lands here, between frames, before anything has been measured against the type it replaces.

Whether the framework has asked for a repaint since clear_dirty.

pub fn is_dirty(&self) -> bool

Marks the pending repaint as pushed; call after presenting the frame.

pub fn clear_dirty(&self)

Each of these is an edge, true for the frame it is reported in and cleared by the next begin_frame.

Reports button as pressed this frame.

pub fn press(&self, button: Button)

Reports button as released this frame.

pub fn release(&self, button: Button)

Reports a completed tap at at.

pub fn tap(&self, at: Point)

Reports a completed swipe.

pub fn swipe(&self, direction: SwipeDir)

Feeds the frame, for a caller that has its own event source.

pub fn input(&self, feed: impl FnOnce(&mut InputState))

The closure runs with the backend's state borrowed, so it must not call back into the backend: no drawing, no screen_size. Feed input and return. What it can write is InputState.

Borrows the display, for pushing the framebuffer to a panel.

pub fn with_display<R>(&self, body: impl FnOnce(&mut D) -> R) -> R

The closure holds the backend's state, so flush the panel, read the pixels, and return without drawing through the backend.

Takes the display out of the backend, until the loan is dropped.

pub fn loan_display(&self) -> Option<DisplayLoan<'_, D>>

For a flush that suspends. with_display holds the guard for as long as its closure runs, and a borrow held across an await is a second task finding the state already borrowed. None when the display is already out. While it is out, anything that paints is discarded and screen_size still answers.

Rust
use xpui_eg::{Backend, Palette};
use xpui_screenshot::Framebuffer;
let backend = Backend::new(Framebuffer::new(480, 800), Palette::INK_IS_ON);
let loan = backend.loan_display().expect("the display is in");
assert!(backend.loan_display().is_none(), "it is out until the loan drops");
assert_eq!(loan.width, 480); // a loan derefs to the display
drop(loan);
assert!(backend.loan_display().is_some());

See also: DisplayLoan, InputState, Palette, Fonts

A display that has been taken out of its backend, and goes back on drop.

pub struct DisplayLoan<'a, D: DrawTarget>

Returned by Backend::loan_display. It derefs, mutably too, to the display, and it borrows the backend only shared, so a loan of a Sync backend is Send and can be held across an await, which is what it is for.

One frame of input, as the event source writes it and the framework reads it.

pub struct InputState

Reached through Backend::input. Every event here is an edge, cleared by begin_frame; what survives it is the held set of buttons, the finger's position until touch_up, and swipe_moves_selection, which is a setting.

FieldMeaning
xpui_eg::InputState::swipe_moves_selectionWhether a swipe up should move focus up rather than dragging content.

Example — a touch driver's frame

Rust
use xpui::host::InputSource;
use xpui::{Button, Point, SwipeDir};
use xpui_eg::{Backend, Palette};
use xpui_screenshot::Framebuffer;
let backend = Backend::new(Framebuffer::new(480, 800), Palette::INK_IS_ON);
backend.begin_frame(16);
backend.input(|state| {
state.press(Button::Confirm);
state.touch_down(Point::new(40, 120));
state.swipe(SwipeDir::Up);
});
assert!(backend.was_pressed(Button::Confirm));
backend.begin_frame(32);
assert!(!backend.was_pressed(Button::Confirm), "an edge lasts one frame");
assert!(backend.is_pressed(Button::Confirm), "the held state survives");

Clears everything that lasts one frame.

pub fn begin_frame(&mut self)

Held buttons survive; that is the difference between "is down" and "went down". Backend::begin_frame calls it.

A button went down: reports both the edge and the held state.

pub fn press(&mut self, button: Button)

A button came up.

pub fn release(&mut self, button: Button)

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

pub fn tap(&mut self, at: Point)

A finger is down at at.

pub fn touch_down(&mut self, at: Point)

Reported every frame it stays down, which is the signal a slider drag needs.

The finger came up.

pub fn touch_up(&mut self)

A completed swipe in direction.

pub fn swipe(&mut self, direction: SwipeDir)

The system back gesture, this frame.

pub fn back_gesture(&mut self)

The system home gesture, this frame.

pub fn home_gesture(&mut self)

Wraps a display, replacing its repeated-pixel path with a per-pixel one.

pub struct PacedFill<D>(D)

Wrap a parallel-bus panel in PacedFill. An 8080-style parallel panel driven through mipidsi shortens a run of one colour into a loop on the write strobe alone, and on a fast microcontroller that loop outruns the controller's minimum write cycle, so the fill lands as noise. It triggers whenever a pixel's two bytes are identical, and ink and background are two such colours, so every filled rectangle and every clear breaks while text, drawn pixel by pixel, comes out perfectly. A panel showing crisp type over static is this fault.

PacedFill routes fill_solid and clear through fill_contiguous, which has a colour for every pixel and so no run to shorten. It is opt-in: an SPI panel clocks its own bytes and a framebuffer has no timing, so neither needs it. hardware.md has the whole account, with the timings.

Rust
use embedded_graphics::pixelcolor::BinaryColor;
use embedded_graphics::prelude::*;
use xpui_eg::{Backend, PacedFill, Palette};
use xpui_screenshot::Framebuffer;
let mut panel = PacedFill::new(Framebuffer::new(320, 240));
panel.clear(BinaryColor::On).unwrap();
let frame = panel.into_inner();
assert_eq!(frame.ink_count(), 320 * 240);
// In a firmware, the wrapped panel is what the backend is built over.
let backend = Backend::new(PacedFill::new(frame), Palette::INK_IS_ON);

Wraps a display so its solid fills go out a pixel at a time.

pub fn new(display: D) -> Self

The display back, for a caller that has finished pacing it.

pub fn into_inner(self) -> D

The two colours ink and background map onto.

pub struct Palette<C>

xpui paints in ink and background, and a palette maps the two onto any PixelColor, so a colour TFT runs the same screens looking monochrome.

FieldMeaning
xpui_eg::Palette::inkWhat ink paints as.
xpui_eg::Palette::backgroundWhat background paints as.

Example — a colour panel

Rust
use embedded_graphics::pixelcolor::Rgb565;
use embedded_graphics::prelude::RgbColor;
use xpui_eg::Palette;
let tft = Palette::new(Rgb565::BLACK, Rgb565::WHITE);
assert_eq!(tft.ink, Rgb565::BLACK);

A palette painting ink as ink and background as background.

pub fn new(ink: C, background: C) -> Self

Ink is On.

pub const INK_IS_ON: Self = Palette

The common case, and what the simulator uses.

Ink is Off.

pub const INK_IS_OFF: Self = Palette

Some 1-bit panels invert: uc8151, the Badger 2040's controller, maps Off to black so bitmaps load unmirrored.

Re-exported so a caller never needs a second dependency that could drift to another version of the same type.

NameWhat it is
xpui_eg::DrawTargetembedded-graphics' drawing trait, the bound on a Backend's display. A caller writing its own fn wire<D: DrawTarget> names this one.
xpui_eg::Labelsxpui-chrome's hint-bar words, for with_labels.
xpui_eg::Metricsxpui-chrome's chrome measurements, for with_metrics.