Docs menu

Interaction

Elements carry the messages they send. The engine finds what the pointer is over, handles focus and the keyboard, scrolls, recognizes gestures, and animates state changes. Your app only reads the messages after the frame.

Actions

An action is any value your app defines, usually a message enum. Attach it to an element; read what fired after the frame:

ui.element(area, div()
    .p_3().rounded_md()
    .hover(|s| s.bg(Tok::CardHi)).transition(motion::INSTANT)
    .on_click(Msg::Open(id))
    .child(text(title)));

for msg in ui.take_actions::<Msg>() {
    app.update(msg);
}
BuilderFires when
on_click(a)A press and release inside the element, or Enter / Space while it has keyboard focus. Makes it focusable.
on_double_click(a)The second of two quick taps (350 ms). The first still clicks.
on_long_press(a)A press held still for 500 ms. Its release does not click.
on_click_outside(a)A press anywhere outside the element: menus and popovers close with it.
on_escape(a)Escape, when focus is inside the element or nothing nearer handles it.
on_drag(|d| …)Each step of a drag that started on the element, with its axis locked after 8 px. A drag the element takes never clicks.
on_swipe(|dir| …)A fast flick that started on the element (500 px/s).

Actions of other types are left for their own take_actions, so a screen and a component can use different message types in the same frame.

Bubbling

An event goes to the topmost element under the pointer, then up through its ancestors. The first one with a handler for it fires, and the event stops there. So a delete button inside a clickable row deletes without also opening the row.

  • .bubble() lets an event an element handled go on to its ancestors as well.
  • .stop_propagation() stops events at an element that has no handler of its own.
  • .disabled(true) greys the element and everything inside it out, and swallows its input.

Hit testing respects clips, scroll containers and open modals, and on touch screens anything with a pointer handler gets a hit box of at least 48 dp, without changing how it looks.

Hover, press and focus styles

A row at rest: a file icon and the label Rest, on a dark card.
Rest
The same row while the pointer rests on it: a lighter card.
Hover
The same row while pressed: a darker card.
Pressed
The same row with keyboard focus: a light ring around it.
Focused (Tab)
The same row disabled: dimmed text and icon.
Disabled

The same row in each state, from one row() function, with input driven by the engine.

State styles change paint only (background, border, text color, opacity), so they never cause layout to move. .transition(seconds) blends them:

div()
    .bg(Tok::Card).border_1()
    .hover(|s| s.bg(Tok::CardHi))
    .active(|s| s.bg(Tok::Border))
    .focus(|s| s.border_color(Tok::Ring))
    .transition(motion::INSTANT)

Use the ADL motion tokens: motion::INSTANT (90 ms) for hover and press, motion::FAST (160 ms) for focus. A .hover applies while the pointer is over the element or anything inside it.

Keyboard and focus

  • Anything with on_click joins the Tab order, in tree order. .focusable(true) adds anything else; .focusable(false) takes a clickable element out.
  • Tab and Shift+Tab move focus and show the focus ring. Enter or Space clicks the focused element.
  • Escape goes to the nearest on_escape around focus, or else to the innermost one on screen.
  • While a modal is open (ui.begin_modal), Tab and Escape stay inside it.
  • A press focuses what it lands on without showing the ring, so Tab continues from there.

Scrolling

.scroll() makes a vertical scroll container. Give it a height (or .flex_1() in a column with a height) and an id:

div()
    .id("inbox")
    .scroll()
    .flex_1()
    .gap_2()
    .children(messages.iter().map(row))

It scrolls with the wheel, a drag and touch fling, with a rubber band at the ends and a scroll thumb. Only the innermost container under the pointer takes input. Its children keep their own height instead of squeezing to fit. A press that scrolled never also clicks the row it started on.

A list of sixteen rows in a scroll container, scrolled down: the top row is cut off, and a scroll thumb sits on the right edge.
A scroll container after three wheel notches. Its rows clip at the edges, and the thumb shows the position.

Gestures

One recognizer serves the whole app with the same thresholds everywhere: tap, double tap (350 ms), long press (500 ms), drag with an axis lock after 8 px, and swipe (500 px/s). Elements use it through the actions above. Immediate-mode code can read this frame's gestures directly:

use anvil_ui::gesture::{Gesture, Phase};

for g in ui.gestures() {
    if let Gesture::Drag(d) = g {
        if d.phase == Phase::Move {
            offset += d.delta;
        }
    }
}

Entrances and transitions

  • .enter(Enter::Rise) fades the element in while it rises 8 px, the first time it is shown. Enter::Fade only fades.
  • .enter_at(Enter::Rise, index) delays it by index stagger steps (40 ms each, at most five), for lists. Give list items ids so each plays once.
  • Motion is skipped when the user has asked for reduced motion (ui.set_reduced_motion(true)).

For anything else that moves, ui.animate, ui.spring and ui.presence are covered in Motion.