Modifiers
Wrappers that change how any view is sized, or what it does when touched,
without the view knowing. Every view has them as methods, so a tree reads as a
chain: Modifiers::<Msg>::frame(Text::new("-"), 44, 44).on_tap(Msg::Down) is a
glyph in a square that sends a message.

The containers they sit in are in Layout. Translating a
component's messages into a screen's, .map, is
ViewExt::map.
Topics
Section titled “Topics”Modifiers | Chainable modifiers, available on every view. |
Frame | Gives a view a fixed size, centring it in the space that makes. |
Flexible | Makes any view absorb leftover space along its parent's stacking axis. |
Tappable | Makes any view report a touch as a message. |
The modifiers at a glance
Section titled “The modifiers at a glance”| Modifier | Wraps the view in | Effect |
|---|---|---|
.on_tap(message) | Tappable | A touch, and Confirm while focused, send message. |
.on_touch(message) | Tappable | A touch sends message; the view stays out of the focus order. |
.on_long_press(message) | Tappable | As .on_tap, and a finger held for 500 ms sends message too, once. |
.flexible() | Flexible | Absorbs leftover space in a stack, the way a Spacer does. |
.frame(width, height) | Frame | Fixes the size and centres the view in it. |
.map(convert) | Mapped | Folds a component's messages into this screen's. |
Order matters. Each modifier wraps what is to its left, so the chain reads inside out:
| Chain | What it does |
|---|---|
.frame(44, 44).on_tap(m) | The whole 44-pixel square is the touch target. What a glyph a few pixels wide wants. |
.on_tap(m).frame(44, 44) | The target grows from the glyph to the theme's minimum touch size, centred on the glyph, and not necessarily to the square. |
.on_tap(m).flexible() · .flexible().on_tap(m) | The same: a tappable view that is flexible. Tappable passes flexibility through. |
.flexible().frame(w, h) | Not flexible. A frame states a size, and hides the flexibility of what it holds. |
Modifiers
Section titled “Modifiers”Chainable modifiers, available on every view.
pub trait Modifiers<M>: View<M> + SizedEvery view implements it, through a blanket implementation for anything that is
a View<M>. Nothing implements it by hand.
Example — a brightness row
use xpui::{Alignment, HStack, Modifiers, Slider, Text, hstack};
#[derive(Clone, Copy)]enum Msg { Set(i32), Down, Up,}
let row: HStack<Msg> = hstack![8; Modifiers::<Msg>::frame(Text::new("-"), 44, 44).on_tap(Msg::Down), Slider::new(30, 100).on_change(Msg::Set).flexible(), Modifiers::<Msg>::frame(Text::new("+"), 44, 44).on_tap(Msg::Up),].align(Alignment::Center);Example — a tap on a bare view
use xpui::{Modifiers, Text, Tappable};
#[derive(Clone, Copy)]enum Msg { Decrement,}
// `message` says which type is meant, so nothing needs naming.let minus: Tappable<Text, Msg> = Text::new("-").on_tap(Msg::Decrement);Required methods
Section titled “Required methods”None: every method has a body.
Provided methods
Section titled “Provided methods”Modifiers::on_tap
Section titled “Modifiers::on_tap”Report a touch on this view, or Confirm while it holds focus, as message.
fn on_tap(self, message: M) -> Tappable<Self, M>The view becomes a focus stop, in the order the tree reads, so Up and Down reach it and a finger and a button produce the same message. The message is cloned each time it is sent, so it is a value, never a closure.
Modifiers::on_touch
Section titled “Modifiers::on_touch”Report a touch as message, without joining the focus order.
fn on_touch(self, message: M) -> Tappable<Self, M>For controls that are meant for a finger and would otherwise add a stop that buttons have no way to activate. On a device with no touch panel the view does nothing at all.
Modifiers::on_long_press
Section titled “Modifiers::on_long_press”Report a press held past the long-press threshold as message, in addition to an ordinary tap.
fn on_long_press(self, message: M) -> Tappable<Self, M>It declares a tap, a focus stop and a long press, all sending the same
message. A quick tap sends it on release, Confirm sends it while the view
holds focus, and a finger resting on the view sends it once the hold reaches
500 ms, the delay before a held key starts repeating.
One press sends one message. A hold fires while the finger is still down, and the release that ends it sends nothing, so a view never hears a long press followed by a tap. A finger that slides off the view before the threshold fires no long press. A hold the loop could not see, because the panel was busy refreshing, starts timing again from the frame that next sees it.
A hold that means something else. Keep the tap on the view and put a
hold-only region around it with Tappable::accepting. A
tap resolves to the innermost region that takes a tap, and a hold to the
innermost region that takes a hold, so each finds its own message.
Example — open on a tap, a menu on a hold
use xpui::{InputMask, Interactions, Modifiers, Point, Tappable, Text, View, testing};
#[derive(Clone, Copy, Debug, PartialEq)]enum Msg { Open, Menu,}
testing::install();let mut book: Tappable<_, Msg> = Tappable::new(Text::new("Middlemarch").on_tap(Msg::Open), Msg::Menu) .accepting(InputMask::LONG_PRESS);book.measure(testing::screen());let mut out = Interactions::new(0);book.interactions(Point::ORIGIN, &mut out);
// Resolved the way the runtime resolves them: the innermost region that takes it.let innermost = |kind: InputMask| { let item = out.items().iter().rev().find(|item| item.mask.contains(kind))?; Some(item.trigger.resolve(item.rect, item.rect.x()))};assert_eq!(innermost(InputMask::TAP), Some(Msg::Open));assert_eq!(innermost(InputMask::LONG_PRESS), Some(Msg::Menu));Modifiers::flexible
Section titled “Modifiers::flexible”Absorb the space fixed-size siblings leave along the stacking axis.
fn flexible(self) -> Flexible<Self>See Flexible for which views grow into that space and which do
not.
Modifiers::frame
Section titled “Modifiers::frame”Fix this view's size, centring it in the frame; an axis of zero or less stays natural.
fn frame(self, width: i32, height: i32) -> Frame<Self>| Parameter | Meaning |
|---|---|
width | The frame's width in pixels, or 0 to keep the view's own. |
height | The frame's height in pixels, or 0 to keep the view's own. |
Shorthand for Frame::new(view).width(width).height(height).
See also: Frame, Flexible, Tappable, ViewExt::map
Gives a view a fixed size, centring it in the space that makes.
pub struct Frame<V>
A "-" glyph is a few pixels wide, but the control it stands for is a row-height
square. Framing it keeps the glyph where the eye expects it, and gives
Tappable a sensible rectangle to grow from. A frame draws
nothing of its own.
| Builder | Sets | When not called |
|---|---|---|
Frame::new | the view inside | — |
width | a fixed width | the view's own width |
height | a fixed height | the view's own height |
How it measures:
- A fixed axis is what the child is offered, so a flexible child fills the frame rather than overflowing it. A natural axis offers the child what the frame was offered.
- A frame is never smaller than its child. A child larger than the fixed size makes the frame grow to fit it; nothing is clipped.
- The child is centred on both axes, and its touch targets move with it.
- A frame always counts towards its stack's extent across, even around a
Spacer, because it states a size on purpose. It is never flexible, whatever it holds.
Example — two glyphs in square tap zones
use xpui::{Divider, Modifiers, Text, VStack, hstack, vstack};
let comparison: VStack<()> = vstack![8; hstack![0; Text::new("-"), Text::new("+")], Divider::new(), hstack![0; Modifiers::<()>::frame(Text::new("-"), 44, 44), Modifiers::<()>::frame(Text::new("+"), 44, 44), ],];Example — one axis fixed, and a child larger than the frame
use xpui::{Frame, Size, Text, View};
let offered = Size::new(480, 800);let mut text = Text::new("Battery");View::<()>::measure(&mut text, offered);let natural = View::<()>::size(&text);
let mut row_height = Frame::new(Text::new("Battery")).height(44);View::<()>::measure(&mut row_height, offered);assert_eq!(View::<()>::size(&row_height), Size::new(natural.width, 44), "the width stays natural");
let mut too_small = Frame::new(Text::new("Battery")).width(4).height(4);View::<()>::measure(&mut too_small, offered);assert_eq!(View::<()>::size(&too_small), natural, "the frame grows to its child");Creating a frame
Section titled “Creating a frame”Frame::new
Section titled “Frame::new”child with neither dimension fixed yet.
pub fn new(child: V) -> SelfA frame with neither axis fixed is exactly its child's size. Usually reached
through Modifiers::frame instead.
Sizing
Section titled “Sizing”Frame::width
Section titled “Frame::width”Fixes the width, or leaves it natural when width is zero or less.
pub fn width(self, width: i32) -> SelfFrame::height
Section titled “Frame::height”Fixes the height, or leaves it natural when height is zero or less.
pub fn height(self, height: i32) -> SelfSee also: Modifiers::frame, Padding, Tappable
Flexible
Section titled “Flexible”Makes any view absorb leftover space along its parent's stacking axis.
pub struct Flexible<V>
A stack measures its fixed children against the whole space it was offered, and
its flexible children afterwards, against a share of what is left; see
how a stack measures. A slider fills whatever
width it is offered, so between fixed - and + glyphs it takes the whole row
unless it is flexible, and the + lands past the edge.
Outside a stack a flexible view is measured as it would be without the wrapper. It draws, declares and measures exactly as its child does.
Example — a slider between two glyphs
use xpui::{HStack, Modifiers, Slider, Text, hstack};
let row: HStack<()> = hstack![8; Text::new("-"), Slider::new(40, 100).flexible(), Text::new("+")];Example — a flexible text grows no wider
use xpui::{HStack, Modifiers, Size, Text, View, hstack};
let mut text = Text::new("Battery");View::<()>::measure(&mut text, Size::new(480, 800));
let mut row: HStack<()> = hstack![0; Modifiers::<()>::flexible(Text::new("Battery"))];row.measure(Size::new(480, 800));assert_eq!(row.size().width, View::<()>::size(&text).width);Creating a flexible view
Section titled “Creating a flexible view”Flexible::new
Section titled “Flexible::new”child, grown into whatever its stack has left.
pub fn new(child: V) -> SelfUsually reached through Modifiers::flexible instead.
See also: Spacer, Modifiers::flexible
Tappable
Section titled “Tappable”Makes any view report a touch as a message.
pub struct Tappable<V, M>The visible size is unchanged; only the hit area grows, to at least the theme's minimum touch target on each axis, centred on what was drawn. A 6-pixel "-" glyph is a legitimate control, and an impossible thing to hit with a finger, so the target is widened around it. Nothing is drawn to show the target, or to show focus: a view that should look focused draws that itself.
| Builder | Sets | When not called |
|---|---|---|
Tappable::new | the view and the message it sends | — |
accepting | which kinds of input it answers | a tap, and focus with Confirm |
The view's own touch targets are declared before the tappable's, so wrapping a
container still resolves a touch to the innermost control when both cover the
point. A tappable is as flexible as its child. It is a view only when M is
Clone, because the message is cloned each time it is sent.
Example — the three ways to respond
use xpui::{Icon, IconRef, Modifiers, Text};
#[derive(Clone, Copy)]enum Msg { Decrement, ShowContextMenu, Toggle,}
Text::new("-").on_tap(Msg::Decrement);Text::new("Wi-Fi").on_long_press(Msg::ShowContextMenu);Icon::new(Glyph::Sun).on_touch(Msg::Toggle); // touch only, out of the focus orderExample — built directly
use xpui::{InputMask, Tappable, Text};
// The same as `Text::new("Close").on_touch(())`.let close: Tappable<Text, ()> = Tappable::new(Text::new("Close"), ()).accepting(InputMask::TAP);Creating a tappable view
Section titled “Creating a tappable view”Tappable::new
Section titled “Tappable::new”child, sending message when tapped or activated.
pub fn new(child: V, message: M) -> SelfChoosing the input
Section titled “Choosing the input”Tappable::accepting
Section titled “Tappable::accepting”Replaces the accepted input kinds, which are InputMask::DEFAULT (a tap plus button focus) until this is called.
pub fn accepting(self, mask: InputMask) -> Self| Parameter | Meaning |
|---|---|
mask | The kinds of input, combined with union or |. TAP is a touch released inside the target. FOCUS makes it a stop Up and Down reach and Confirm fires. LONG_PRESS sends the message once, when a finger has rested on the target for 500 ms; see on_long_press. |
InputMask::DRAG offers every frame the finger is down to its target, so a
tappable accepting it sends its message on each of those frames. It is for a
control that reads the position, which a Tappable does not.
See also: Modifiers::on_tap, Frame