Device Input (Sensors, Gamepad, Geolocation)
Introduction
Motion sensors (accelerometer / gyroscope / magnetometer), gamepads, and
geolocation are event-driven: you attach a callback to a window-level event
filter and the framework invokes it when a new sample arrives. You do not
poll on a Timer. Inside the callback you read the current value
through an accessor on CallbackInfo. This is the same
„react to an event, then read the detail“ shape as a key press or a mouse move.
This avoids the busy-loop a poll would cause and keeps the device's sample rate in charge of how often your code runs.
Motion sensors
Attach a callback to WindowEventFilter::SensorChanged; it fires when any
sensor reading advances. Read the value with CallbackInfo::get_sensor_reading:
let dom = Dom::create_body().with_callback(
EventFilter::Window(WindowEventFilter::SensorChanged),
state.clone(),
on_sensor,
);
extern "C" fn on_sensor(mut data: RefAny, info: CallbackInfo) -> Update {
if let OptionSensorReading::Some(r) = info.get_sensor_reading(SensorKind::Accelerometer) {
if let Some(mut s) = data.downcast_mut::<MyState>() {
s.tilt_x = r.x; // m/s^2 on each axis; r.magnitude() for total
}
}
Update::RefreshDom
}
There is also a node-level HoverEventFilter::SensorChanged mirror, for the
„redraw this node when a reading changes“ pattern. Identical consecutive
readings do not re-fire (an idle sensor streaming the same value is silent).
Gamepad
Attach to WindowEventFilter::GamepadInput; read state with
CallbackInfo::get_primary_gamepad (the first connected pad - the common
single-controller case) or get_gamepad_state(id):
extern "C" fn on_gamepad(mut data: RefAny, info: CallbackInfo) -> Update {
if let OptionGamepadState::Some(pad) = info.get_primary_gamepad() {
let jump = pad.is_pressed(GamepadButton::South); // A / Cross
let move_x = pad.left_stick_x; // [-1, 1]
// ... drive your game/menu state
}
Update::RefreshDom
}
GamepadState carries connected, a buttons bitset (read via is_pressed),
two sticks, and the triggers (left_z / right_z). A disconnect keeps the slot
with connected = false so you can observe it.
Geolocation
Geolocation follows the WebAuthn-style permission model: mounting a
Dom::create_geolocation_probe(...) node is the permission request. Once a
backend delivers a fix you read it with CallbackInfo::get_location_fix:
// Mounting the probe asks the OS for location.
container.with_child(Dom::create_geolocation_probe(GeolocationProbeConfig {
high_accuracy: true,
background: false,
max_accuracy_m: 0.0,
min_interval_ms: 0,
}));
// Read the latest fix from any callback:
if let OptionLocationFix::Some(fix) = info.get_location_fix() {
let (lat, lon) = (fix.latitude_deg, fix.longitude_deg);
}
LocationFix carries latitude/longitude, accuracy, altitude (+ accuracy),
heading, speed, and a timestamp. See the AzulMaps example for a live readout.
Pen / stylus
Pen input (Apple Pencil, S-Pen, Wacom) is state-based, like sensors: react
to ordinary pointer events (MouseDown / MouseMove / MouseUp) and read the pen
detail with CallbackInfo::get_pen_state (or get_pen_pressure /
get_pen_tilt). PenState carries position, pressure, tilt, the contact +
eraser flags, the barrel button, and the extended axes (tangential pressure,
barrel roll). The platform backend populates it through the gesture manager;
examples/azul-paint uses exactly this to draw pressure-modulated,
barrel-roll-oriented strokes.
extern "C" fn on_pointer_move(mut data: RefAny, info: CallbackInfo) -> Update {
if let Some(pen) = info.get_pen_state().into_option() {
let width = 2.0 + pen.pressure * 10.0; // light touch -> thin line
// ... draw with `width`, oriented by pen.barrel_roll, erase if is_eraser
}
Update::RefreshDom
}
(The PenDown/Move/Up event filters exist but are not the path apps use -
pointer-event + get_pen_state is.)
Safe-area insets (notches)
On displays with a notch, rounded corners, or system bars (a MacBook notch, a
phone, a tablet) part of the window is obscured. Read the insets - the margins
to keep interactive content clear of - with CallbackInfo::get_safe_area_insets:
extern "C" fn on_event(data: RefAny, info: CallbackInfo) -> Update {
let safe = info.get_safe_area_insets(); // css SafeAreaInsets
if let Some(top) = safe.top.into_option() {
// ... pad the top bar by `top` so it clears the notch
}
Update::DoNothing
}
SafeAreaInsets carries top / right / bottom / left, each an
OptionPixelValue (None where there is no inset). It's the same
azul_css::SafeAreaInsets the styling system holds, so the values are shared
with layout. The platform shell populates them - macOS reads the notch via
NSView.safeAreaInsets; iOS (UIView.safeAreaInsets) and Android
(WindowInsets) are being wired. Zero on displays with no inset.
Configurability
- Sensors: the manager keeps one reading per
SensorKind; request rate / which sensors are platform-backend concerns (CoreMotion / AndroidSensorManager). - Gamepad: deadzone + rumble are platform-backend features; the cross- platform surface gives you raw stick/trigger values and the button bitset.
- Geolocation:
GeolocationProbeConfigconfigures accuracy, background delivery, max accuracy, and minimum interval at mount time.
Testing without hardware
Every path above is exercised synthetically (no device) by
layout/tests/synthetic_events.rs: it injects readings/states/fixes through the
same push_sensor_reading / push_gamepad_state / push_location_fix channels
the real platform backends use, then asserts the manager folds them and the
EventProvider yields SensorChanged / GamepadInput. See
e2e-testing. The pen path is exercised via
GestureAndDragManager::update_pen_state -> get_pen_state.
See also
- events - the event-filter + dispatch model.
- callbacks -
CallbackInfoaccessors +RefAny. - Realtime Media and Devices - camera/mic capture.