compose-rust guide

Reference

Widget reference

Every widget, modifier and role in compose-rust's schema. The schema is closed: the renderer interprets exactly this list and nothing else. A widget kind it does not recognise produces a ProtocolError event rather than a crash.

What exists today

The schema and the renderer half of every widget below exist in 0.0.1 Works. The Rust functions you call to emit them, such as Column(...) and Text(...), arrive with the authoring API in 1.0.0 Planned. The authoring API adds no widget of its own, so this page is also the list you will write with.

Widgets

The names are Compose's. Each widget has a fixed tag in WidgetKind, and the Kotlin interpreter has one renderer function for each.

GroupWidgetWhat it is
LayoutColumn, RowChildren along one axis, with an arrangement on the main axis and an alignment on the cross axis.
BoxChildren stacked on top of each other, aligned on both axes.
ScrollColumn, ScrollRowA column or row that scrolls. Every child is a real node; for long lists use the lazy widgets.
SpacerEmpty space, usually sized by weight or a fixed size.
SplitPaneTwo panes with a divider the user can drag or move with the keyboard.
Scaffold, ScaffoldSlotA screen's frame: bars, a page, a floating action. The renderer arranges the slots the application filled for the window's width.
FileDropTargetA region that accepts files dropped from the system and reports their paths.
Text and inputTextText, with a type role, colour, alignment, line limit and overflow. Can carry styled runs and accept streamed tails.
TextFieldUncontrolled text input. The edit buffer and IME composition stay in the renderer.
SelectionContainerMakes the text inside it selectable and copyable.
ControlsButtonA button in one of four variants: Filled, Tonal, Outlined, Text.
Checkbox, RadioButton, SwitchFully controlled toggles: what you pass is exactly what is drawn.
SliderA value on a range. The renderer owns the position during a drag.
DropdownA choice among options in a popup.
DatePicker, TimePickerDate and time choosers. The renderer owns the picking surface, locale and format.
ChipA small choice or filter. It draws the chosen state it was sent and keeps none of its own.
ProgressIndicatorDeterminate or indeterminate progress.
ContainersCard, SurfaceFilled panels that take shape, elevation and material from the design system.
TopAppBarA screen's top bar with a title and actions.
DividerA hairline between groups.
Tooltip, BadgeA hint on hover or long press; a mark attached to another widget.
FloatingActionThe one action a screen is about. The design system decides how it looks and where it sits.
ListsLazyColumn, LazyRowWindowed lists. The renderer asks for the visible range and the Host builds only that. See Lists & streaming.
LazyGridA windowed grid with the same contract.
LinearProgressIndicatorThe worked example of an extension widget (tag 100). See Extensions.
NavigationNavigation, NavigationItemA set of destinations. The renderer draws a bar, a rail or a drawer from the width it measured.
TabsTabs across the top of a page.
OverlaysDialog, Menu, SheetContent over the page. The renderer picks the edge a sheet enters from, and owns the enter and exit.
messagesTransient messages are not part of the tree. show_message and Message send them, and they exist in 0.0.1.
PicturesImage, IconAn image from a registered asset; an icon by role.
CanvasCustom drawing from a DrawList value. Two equal lists send nothing.

Modifiers

In Compose a Modifier applies to any composable, and the same is true here: every widget takes every modifier, including Text and Spacer. On the wire a modifier list is a sequence of fixed-size values, and the renderer rebuilds it into a Compose Modifier chain.

ModifierValueEffect
Weightf32Share of the leftover main-axis space inside a Row or Column.
FillMaxWidth, FillMaxHeightnoneTake the whole available width or height.
Width, Height, SizedpA fixed size.
Padding, PaddingEachdpAn inset you chose, the same on all sides or per side.
PaddingRoleSpaceRoleAn inset the design system chose. Prefer this one.
BackgroundPaintA fill behind the widget.
Shapedp per cornerCorner rounding you chose.
ShapeRoleShapeRoleCorner rounding the design system chose.
Borderdp and PaintA border of a width and a colour.
ElevationdpResting height. Each design system draws it its own way.
MaterialMaterialRoleWhat a surface is made of, such as glass, resolved by the design system.
MotionMotionRoleThe design system's timing for this widget's own transitions.
Clickablea handler idMakes any widget report clicks.
ObserveSizea tokenAsks the renderer to report this node's measured size, under a token the application chose, read with node_size.

Unset is not zero. A modifier you did not write sends nothing at all. No padding and a padding of zero look the same on screen, but only one of them should cost a record.

The role vocabulary

Widgets emit roles, not literals. A role names a job ("the accent colour", "the heading rung", "a medium corner"), and the renderer resolves it against the active design system. That is what lets one tree come out as Material 3 on one machine and Liquid Glass on another. Design systems explains how the system is chosen. All of these types exist in 0.0.1.

Paint and Color

Anywhere a colour is taken, the type is Paint: a role, a literal ARGB value, or a registered brush such as a gradient.

rust, compiles against 0.0.1
use compose_rust::{Color, ColorRole, Paint};

let accent = Paint::Role(ColorRole::Primary);          // resolved by the design system
let brand = Paint::Literal(Color::rgb(0x1b6ef3));     // opaque, 0xRRGGBB
let veil = Paint::Literal(Color::argb(0x80ff0000));    // 0xAARRGGBB

A literal is the escape hatch. It survives a design-system change unchanged. That is sometimes what you want, such as a brand colour, and usually not, such as a colour that reads on a light page and disappears on a dark one. For brand colours, a Palette is better; see Application colours.

ColorRole: 45 roles

Every role that names ink is paired with the surface it reads on. OnPrimary is the ink for something filled with Primary, and so on.

GroupRolesFor
AccentPrimary, OnPrimary, Secondary, OnSecondary, Tertiary, OnTertiaryThe three accents: what the eye goes to first, second and third.
PageBackground, OnBackgroundThe page itself and the ink on it.
SurfaceSurface, OnSurface, SurfaceVariant, OnSurfaceVariant, SurfaceContainerReading surfaces. OnSurfaceVariant is the muted ink, for captions and labels.
ContainerPrimaryContainer, OnPrimaryContainer, SecondaryContainer, OnSecondaryContainer, TertiaryContainer, OnTertiaryContainerQuiet fills that read as relatives of their accent, each with its own ink.
LineOutline, OutlineVariantBorders and hairlines, strong and quiet.
ErrorError, OnErrorWhat went wrong, and a destructive action's label.
CodeSyntaxKeyword, SyntaxString, SyntaxComment and the other syntax roles, plus DiffAdded, DiffRemoved, DiffModified and their containers and emphasisTwenty-two roles that paint code. Each system answers them from its platform's own code editor, and every syntax ink reads at 4.5:1 on SurfaceContainer.

Reach for SurfaceContainer when a panel has to be visible against the page. Material 3 gives Surface and Background the same value on purpose and shows depth through tonal containers instead. A panel filled with Surface on a page of Background is drawn correctly and cannot be seen. Every supported design system makes SurfaceContainer visible against the page.

TypeRole: nine rungs

RungFor
DisplayThe largest thing on a screen, used once.
HeadlineA screen's title.
TitleA section's title, a bar's title.
SubtitleThe line under a title.
BodyParagraphs. The default rung.
BodyStrongBody text that has to carry weight.
LabelA button's text, a chip, a field label.
CaptionThe smallest readable rung: timestamps, footnotes.
MonoMonospace: code, paths, figures that have to line up.

ShapeRole, SpaceRole and MotionRole

ShapeRole is None, ExtraSmall, Small, Medium, Large and Full. The radius is the design system's answer, and Full means a capsule.

SpaceRole is None, Xs, Sm, Md, Lg, Xl and Xxl. Density differs per design system, which is the reason to spell a gap as a role rather than a number of dp.

MotionRole is Instant, Quick, Standard, Slow and Emphasized. Each design system gives them its own duration and curve.

IconRole

An icon is addressed by what it is for, never by a platform icon name: Back, Forward, Close, Search, Add, Check, Settings, More, Home, List, Inbox, Menu and History. The role is what lets one declaration come out as an SF Symbol under Apple's systems and a Material Symbol under Material 3.

Layout and text enums

EnumVariantsWhere
ArrangementStart, Center, End, SpaceBetween, SpaceAround, SpaceEvenlyMain axis of Row and Column
AlignmentTopStart through BottomEnd, nine in allCross axis of Row and Column; both axes of Box
TextAlignStart, Center, End, JustifyText
TextOverflowClip, Ellipsis, VisibleText with a line limit
ButtonVariantFilled, Tonal, Outlined, TextButton
MessageDurationShort, LongTransient messages

Window size

The UI is described in Rust and measured by the renderer, so the only way Rust learns the window's width is for the renderer to report it. window_size() returns a WindowSize with width_dp, height_dp and class, plus is_compact(), is_medium() and is_expanded(). It exists in 0.0.1.

ClassWidthShape
WindowSizeClass::Compactunder 600 dpPhone-shaped. One column.
WindowSizeClass::Medium600 to 840 dpTablet-shaped.
WindowSizeClass::Expanded840 dp and widerDesktop-shaped. Room for a permanent sidebar.

Only a class change is reported. The renderer sends one event when the class changes and nothing in between. Dragging a window from 700 dp to 800 dp wakes nothing in Rust, because a size that changed every layout pass would run Rust code every layout pass. Before the first measurement the value is Compact at 0 by 0, because a one-column layout works at any width.

Navigation, Sheet and Scaffold already adapt without being asked, by moving nodes the renderer already has. Use the window size for the decisions the widgets cannot make for you, such as two columns instead of one.

Where each widget keeps its state

Application state is yours, and UI-local state is the renderer's. These are the places that surprise people, because a widget looks controlled and is not, or the other way round.

WidgetWhat the renderer ownsWhat Rust sends
TextFieldThe whole edit buffer, the selection, the IME compositionNo value. Changes arrive as events; SetText replaces the text after composition ends.
Dialog, Sheet, MenuOpen state, and the enter and exit animationAn open flag that seeds it and carries an outside change.
DropdownWhether the popup is openThe selected index only.
Navigation, TabsWhich one is marked, and the move between themThe selected index, which seeds it.
SliderThe position a drag is passing throughThe value, which seeds it and moves it from outside.
ScrollColumn, ScrollRow, the lazy widgetsScroll position, and the read-ahead bufferThe item count and the items in the requested range.
Checkbox, RadioButton, Switch, ChipNothing. These are fully controlledThe checked or selected state, which is exactly what is drawn.
DatePicker, TimePickerThe picking surface, the locale, the time zone, the formatThe value and its bounds.
every widgetFocus, hover, press feedback, animation progressNothing.

Extensions

An extension is a compile-time pair of source files, one Rust and one Kotlin, built into both sides. It is not a runtime plugin: a deployed renderer cannot load a new widget, because the schema hash would no longer match. The crate ships one worked example, LinearProgressIndicator, with a single progress: f32 property.

Adding your own means registering the widget and property tags in compose-rust/src/extensions.rs and rendering the new kind in a paired Kotlin renderer. Extension widget tags start at 100, so they never collide with a core widget added later.