State Machine
Every Ui-Node has an internal state machine, represented by the UiState component.
States are not booleans — each state has a weight between 0.0 and 1.0, which is smoothly
animated over time. The weights are then used to blend together the layouts and colors you
defined for each state, which makes transitions look fluid without any extra effort.
The state with the weight is called the active state and all others are inactive.
Built-in states
UiBase- The default state every node starts with (always weight1.0when no other state is active).UiHover- Enabled while the pointer is over the node, smoothly interpolates using configurable speeds.
Note
There are additional states in the works (
UiSelected,UiClicked,UiIntro,UiOutro), but they are work in progress and not yet wired up.
Enabling a state
A state first needs to be enabled for the entity by adding its component. The most common one is hover, which is driven by picking observers:
UiHover- The state component with transition settings.forward_speed- How fast the weight goes towards1.0when enabled.backward_speed- How fast the weight falls back towards0.0when disabled.instant- Skip the animation entirely.
Example
ui.spawn((
// Like this you can enable a state
UiHover::new().forward_speed(20.0).backward_speed(4.0),
// You can define layouts per state
UiLayout::new(vec![
(UiBase::id(), UiLayout::window().full()),
(UiHover::id(), UiLayout::window().x(Rl(10.0)).full())
]),
// You can define colors per state
UiColor::new(vec![
(UiBase::id(), Color::srgba(1.0, 0.0, 0.0, 0.8)),
(UiHover::id(), Color::srgba(1.0, 1.0, 0.0, 1.0))
]),
// ... Sprite, Text, etc.
// Add observers that enable/disable the hover state component
)).observe(hover_set::<Pointer<Over>, true>)
.observe(hover_set::<Pointer<Out>, false>);
The hover_set utility is a ready-made observer that toggles the hover state on the entity it is
attached to. The generic true/false constant decides whether the state should be enabled or
disabled, so you pair it with Pointer<Over> and Pointer<Out> respectively.
Tip
Hover events are automatically duplicated to all children of the observed entity, so hovering a parent node enables hover on the whole subtree. Attach the observers to the outermost node.
How the blending works
For each frame, Lunex computes the rectangle of every state’s layout and then normalizes the weights.
If, for example, hover is at 0.5, the resulting node position and size are exactly halfway between
the UiBase layout and the UiHover layout. The same applies to colors, which are blended in HSLA
space (the hue is interpolated along the shortest arc).
If no state is active at all, the UiBase layout and color are used as fallback.
Custom states
You can define your own states by implementing UiStateTrait for a component:
value()- Returns the current weight, expected to be within0.0 - 1.0. Any smoothing should happen inside this function.
#[derive(Component)]
struct UiWiggle {
value: f32,
}
impl UiStateTrait for UiWiggle {
fn value(&self) -> f32 {
self.value
}
}
You then reference the state by its id() when defining layouts and colors, the same way as
built-in states. To pipe the value into the state machine, use the generic system
system_state_pipe_into_manager::<UiWiggle> — it reads the component, writes the value into
UiState and triggers RecomputeUiLayout for the affected nodes.
app.add_systems(Update, system_state_pipe_into_manager::<UiWiggle>);
Warning
The weight of
UiBaseis automatically balanced to1.0 - (sum of all other states), so you never define it manually. If your custom state is at1.0, the base layout is fully faded out.