Skip to content

Theme

What a widget asks the installed theme for: the geometry it lays out with, the furniture the theme paints itself, and the way a control shows what the keys will do to it next. Theme is a façade of associated functions on a zero-sized type, so nothing threads a host reference through every call.

FaçadeFor
ThemeThemed furniture and the metrics behind it.
RendererDrawing primitives, screen_size and screen_bounds. Mostly for widget authors.
ScreenChromeThe header band and the button hints. Used by the screen roots.
InputOne frame of buttons, touch and gestures.

Renderer, ScreenChrome and the two free functions, millis and request_update, are in drawing and repainting; the navigation pair is in navigation.

Writing a widget builds a view that draws with the theme. The host guide is the other side of these calls: the traits a backend implements to answer them, listed on the backend contract. This page is what each piece does.

ThemeThe active theme, as widgets reach for it.
ThemeMetricA geometry value from the active theme.
ControlStateWhat the keys will do to a control next, so a person can see it: a value row on a device with no Left/Right pair changes what four keys mean when it opens, and the panel has to say so.

Every call goes to whatever was installed. Theme::metric asks the installed Chrome, Renderer::fill_rect the installed Canvas, millis the installed Clock. The façades hold nothing and cache nothing, so a theme the user changes between frames is the theme the next frame measures with.

Nothing works before a host is installed. A call with no host panics, because a screen cannot be measured before its host exists. A build with the testing feature falls back to the fake host instead, so a test or a doctest can call these straight away; testing::install() says so explicitly.

Coordinates are logical screen pixels in the current orientation, with the origin at the top-left: see geometry. A view's render receives its absolute origin, so a widget draws at origin, never at 0, 0.

The active theme, as widgets reach for it.

pub struct Theme

Ask the theme for geometry rather than writing pixel offsets. A header, a list row and a slider knob are as tall as the host's theme says, and the panels this runs on range from 296x128 to 800x480. A screen laid out from content_area and metric needs no changes when either the theme or the panel does.

The host paints the furniture. A list, a slider, a progress bar, a sub-header, a scroll indicator and an option popup are drawn by the host's Chrome, not by the framework, so a Rust screen and a native one are the same pixels and follow the user's theme. The framework says where each goes and what it shows; the widgets on the other reference pages call these functions for you, and a custom widget calls them directly.

MemberAnswers or draws
metricone ThemeMetric, in pixels
content_areathe rect between the header and the hints
draw_sub_headera section heading
draw_progress_bara progress bar
draw_slidera slider track and knob
draw_scroll_indicatora scroll indicator
draw_lista list of rows
draw_option_popup · option_popup_row_rectan option popup, and where its rows are

Example — laying out from the theme, not from pixels

Rust
use xpui::{Rect, Renderer, Theme, ThemeMetric, testing};
testing::install();
let content = Theme::content_area(); // between header and hints, already inset
let row = Theme::metric(ThemeMetric::ListRowHeight);
let gap = Theme::metric(ThemeMetric::ListRowGap);
// The third row of a list that starts at the top of the content area.
let third = Rect::new(content.x(), content.y() + 2 * (row + gap), content.width(), row);
assert!(content.contains(third.origin));
assert!(third.bottom() <= content.bottom());
assert!(content.width() < Renderer::screen_size().width);

Example — a heading widget drawn by the theme

Rust
use xpui::{Point, Rect, Size, Theme, ThemeMetric, View, testing};
/// A section heading, as tall as the theme's heading line.
struct Heading {
label: &'static str,
size: Size,
}
impl<M> View<M> for Heading {
fn measure(&mut self, available: Size) {
let height = Theme::metric(ThemeMetric::SubHeaderHeight);
self.size = Size::new(available.width, height);
}
fn size(&self) -> Size {
self.size
}
fn render(&self, origin: Point) {
Theme::draw_sub_header(Rect { origin, size: self.size }, self.label, None);
}
}
testing::install();
testing::reset();
let content = Theme::content_area();
let mut heading = Heading { label: "Wi-Fi", size: Size::ZERO };
View::<()>::measure(&mut heading, content.size);
View::<()>::render(&heading, content.origin);
let drawn = testing::drawn_sub_headers();
assert_eq!(drawn[0].0.origin, content.origin);
assert_eq!(drawn[0].0.height(), testing::SUB_HEADER_HEIGHT);
assert_eq!(drawn[0].1, "Wi-Fi");

See Chrome::metric.

pub fn metric(metric: ThemeMetric) -> i32

One value, in pixels, asked for each time. The table under ThemeMetric lists every value and what in the framework reads it.

The region between the header and the button hints, already inset horizontally by the theme's side padding.

pub fn content_area() -> Rect

Its left edge is ContentSidePadding, its top ContentTop, its bottom ContentBottom, and it is as wide as the panel less the padding on both sides. It is not inset vertically: ContentTop already sits below the header's own spacing. A theme whose padding leaves no room gives an empty rect, never an inverted one.

Example — the content area on the test host

Rust
use xpui::{Renderer, Theme, ThemeMetric, testing};
testing::install();
let content = Theme::content_area();
let side = Theme::metric(ThemeMetric::ContentSidePadding);
assert_eq!(content.x(), side);
assert_eq!(content.y(), Theme::metric(ThemeMetric::ContentTop));
assert_eq!(content.bottom(), Theme::metric(ThemeMetric::ContentBottom));
assert_eq!(content.right(), Renderer::screen_size().width - side);

These draw into a rect the caller has already laid out. The widgets that wrap them, Section, ProgressBar, Slider and ScrollView, are what a screen normally uses.

See Chrome::draw_sub_header.

pub fn draw_sub_header(rect: Rect, label: &str, right_label: Option<&str>)
ParameterMeaning
rectWhere the heading goes. Give it SubHeaderHeight, not a list row's height: the theme draws the label top-aligned.
labelThe heading, at the leading edge.
right_labelA value at the trailing edge, or None.

See Chrome::draw_progress_bar.

pub fn draw_progress_bar(rect: Rect, current: u32, total: u32)

Example — a bar at the foot of the content area

Rust
use xpui::{Rect, Theme, ThemeMetric, testing};
testing::install();
testing::reset();
let content = Theme::content_area();
let height = Theme::metric(ThemeMetric::ProgressBarHeight);
let bar = Rect::new(content.x(), content.bottom() - height, content.width(), height);
Theme::draw_progress_bar(bar, 3, 10);
assert_eq!(testing::drawn_progress_bars(), vec![(bar, 3, 10)]);

See Chrome::draw_slider.

pub fn draw_slider(rect: Rect, value: i32, max: i32, state: ControlState)
ParameterMeaning
rectThe track's rect. The host draws the knob inside it, SliderSideInset in from each end.
valueHow far along, from 0 to max.
maxThe value at the right-hand end.
stateWhat the keys will do to it next: see ControlState.

See Chrome::draw_scroll_indicator.

pub fn draw_scroll_indicator(rect: Rect, content: i32, visible: i32, offset: i32)
ParameterMeaning
rectThe window the content scrolls in.
contentThe content's full height.
visibleHow much of it the window shows.
offsetHow far down the window sits.

The host draws nothing when content already fits in visible.

The themed list — see Chrome::draw_list.

pub fn draw_list<'a>(
rect: Rect,
rows: usize,
selected: i32,
row: &dyn Fn(usize, RowField) -> Option<&'a str>,
)
ParameterMeaning
rectThe space the rows fill. The host draws only rows that fit entirely.
rowsHow many rows there are.
selectedThe highlighted row, or -1 for none.
rowCalled per visible row and field. None omits the field, which is how the host tells a one-line row from a two-line one.

List calls this, and adds focus, taps and the height the rows need. RowField is in xpui::host.

Example — rows from a table

Rust
use xpui::host::RowField;
use xpui::{Theme, testing};
testing::install();
testing::reset();
let rows = [("Wi-Fi", Some("Home")), ("Bluetooth", None)];
Theme::draw_list(Theme::content_area(), rows.len(), 0, &|index, field| {
let (title, value) = rows.get(index)?;
match field {
RowField::Title => Some(*title),
RowField::Value => *value,
RowField::Subtitle => None,
}
});
assert_eq!(testing::drawn_lists(), vec![(2, 0)]);

See Chrome::draw_option_popup.

pub fn draw_option_popup<'a>(
title: &str,
options: &dyn Fn(usize) -> Option<&'a str>,
count: usize,
selected: i32,
)
ParameterMeaning
titleThe popup's heading.
optionsCalled per row for its label.
countHow many options there are.
selectedThe highlighted option.

See Chrome::option_popup_row_rect.

pub fn option_popup_row_rect<'a>(
title: &str,
options: &dyn Fn(usize) -> Option<&'a str>,
count: usize,
index: usize,
) -> Option<Rect>

Pass the same title, options and count the popup was drawn with: the host lays the dialog out from them. None means the row is not on screen.

Example — which option a tap landed on

Rust
use xpui::{Theme, testing};
testing::install();
let options = ["Off", "5 min", "15 min"];
let label = |index: usize| options.get(index).copied();
Theme::draw_option_popup("Sleep after", &label, options.len(), 0);
// A tap somewhere inside the third row.
let third = Theme::option_popup_row_rect("Sleep after", &label, options.len(), 2).unwrap();
let tap = third.origin.offset(2, 2);
let hit = (0..options.len()).find(|&index| {
Theme::option_popup_row_rect("Sleep after", &label, options.len(), index)
.is_some_and(|row| row.contains(tap))
});
assert_eq!(hit, Some(2));

See also: ThemeMetric, ControlState, Renderer, Chrome

A geometry value from the active theme.

pub enum ThemeMetric

Asked for one at a time, by tag. A host's metrics table has dozens of fields, and a struct mirroring its layout here would silently read the wrong value the day one was inserted. Each variant carries a fixed u8 tag that crosses the C ABI, which is why the numbers are not in declaration order: ListRowGap was given the next free tag rather than renumbering the rest.

VariantAbstractTagRead by
ThemeMetric::TopPaddingGap above the header band.0OverlayPanel
ThemeMetric::HeaderHeightHeight of the header band.1OverlayPanel
ThemeMetric::VerticalSpacingThe gap between stacked elements.2Stepper, OverlayPanel
ThemeMetric::ButtonHintsHeightThe band reserved at the bottom for the button hints.3nothing in the framework: for a screen's own layout
ThemeMetric::ContentSidePaddingSpace between the panel's side edges and the content.4Theme::content_area, OverlayPanel
ThemeMetric::ContentTopFirst y below the header that content may use.5Theme::content_area
ThemeMetric::ContentBottomFirst y occupied by the button hints; content must stay above.6Theme::content_area
ThemeMetric::ListRowHeightHeight of a one-line list row.7List, Slider, Stepper
ThemeMetric::ListRowHeightWithSubtitleHeight of a list row carrying a subtitle.8List
ThemeMetric::ListRowGapSpace the theme leaves between one row and the next.14List
ThemeMetric::ProgressBarHeightHeight of the themed progress bar.9ProgressBar, when not given a height
ThemeMetric::MinTouchSizeThe smallest comfortably tappable dimension.10IconToggle, and every touch target too small for a finger
ThemeMetric::SliderKnobWidthA slider knob's width.11the conversion from a touch to a slider's value
ThemeMetric::SliderKnobHeightA slider knob's height, and so the least height its track needs.12Slider
ThemeMetric::SliderSideInsetThe padding a slider's track is inset by at each end.13the conversion from a touch to a slider's value
ThemeMetric::SubHeaderHeightHeight of the band a sub-header needs: the heading's own line, not a list row.15Section
ThemeMetric::SpacingSmallThe theme's small step, for space within a group.16Section, a control's title line

Example — the height a list of rows needs

Rust
use xpui::{List, ListRow, Size, Theme, ThemeMetric, View, testing};
testing::install();
let row = Theme::metric(ThemeMetric::ListRowHeight);
let gap = Theme::metric(ThemeMetric::ListRowGap);
let mut list: List<()> = List::new()
.push(ListRow::new("Wi-Fi"))
.push(ListRow::new("Bluetooth"))
.push(ListRow::new("Airplane mode"));
list.measure(Size::new(400, 1000));
// Three rows and the two gaps between them: none after the last.
assert_eq!(list.size().height, 3 * row + 2 * gap);

Example — separating groups by more than the small step

Rust
use xpui::{Theme, ThemeMetric, testing};
testing::install();
let within = Theme::metric(ThemeMetric::SpacingSmall);
let between = Theme::metric(ThemeMetric::VerticalSpacing);
// A heading must sit closer to what it heads than to what is above it.
assert!(between > within);

See also: Theme::metric, Theme::content_area

What the keys will do to a control next, so a person can see it: a value row on a device with no Left/Right pair changes what four keys mean when it opens, and the panel has to say so.

pub enum ControlState

The framework decides the state and the host draws it. The runtime passes one to Theme::draw_slider on every paint, from what holds focus and whether a value is open; what each state looks like is the backend's choice. The value mode is what opens a control.

It crosses the C ABI as a u8, so a host written in C++ can switch on it. The default is Idle.

VariantAbstractTag
ControlState::IdleThe keys are elsewhere, so the control shows the value it holds and nothing more.0
ControlState::FocusedThe keys would act on this control if they moved a value now.1
ControlState::EditingThe control is open: the keys that walked the list are moving this value, Confirm keeps it, and Back drops the edit rather than leaving the screen.2

Example — a custom track drawn in each state

Rust
use xpui::{ControlState, Rect, Theme, ThemeMetric, testing};
testing::install();
testing::reset();
let content = Theme::content_area();
let height = Theme::metric(ThemeMetric::SliderKnobHeight);
let track = Rect::new(content.x(), content.y(), content.width(), height);
for state in [ControlState::Idle, ControlState::Focused, ControlState::Editing] {
Theme::draw_slider(track, 60, 100, state);
}
assert_eq!(testing::drawn_sliders().len(), 3);
assert_eq!(ControlState::default(), ControlState::Idle);
assert_eq!(ControlState::Editing as u8, 2);

See also: Theme::draw_slider, Slider, Stepper